Integration Tools

Hardata ECP (External Control Protocol)

Document version: 1.0
Public contract covered: HARDATA ECP 1.0

Reference status: implementation available as of August 19, 2026

Public, self-contained reference for developers building external HARDATA control applications. The field names, versions, and envelopes shown here are part of the interoperability contract and must be followed exactly.

Important: HARDATA ECP controls HARDATA HDX 5 or DINESAT 13 in real time through WebSocket. To query or manage metadata, media, and advertising traffic over HTTP, use the separate specification HARDATA API.

This document is self-contained. A person or AI should be able to develop a functional client App without consulting source code or additional documentation.

1. Purpose and Mental Model

The App does not connect directly to HDX Radio. It connects by WebSocket to the local Gateway. The Gateway maintains a single Named Pipe connection to HDX Radio, authenticates each logical session, arbitrates who may operate, correlates responses, and distributes state.

App 1 ─┐
App 2 ─┼─ WebSocket JSON ─ HARDATA ECP Gateway ─ Named Pipe JSON ─ HDX Radio 4
App N ─┘                    sesión y control   conexión única

HARDATA ECP 1.0 is the only public protocol. All messages use the root object ecp; the App does not know or interpret the private protocol between Gateway and HDX.

App -- HARDATA ECP 1.0 --> Gateway -- protocolo interno --> HDX Radio

The Gateway handles session management, authorization, control, and translation of commands, responses, and events. This separation allows HDX to evolve without transferring that complexity to client applications.

HARDATA ECP 1.0 is the first version of the public contract. The contract exclusively uses the envelope ecp and the field names defined in this document; there are no legacy public envelopes, aliases, or actions. The HDXRC 1.2 protocol continues to exist only as an internal detail between the Gateway and HDX.

2. Address, Transport, and Limits

Default values:

  • WebSocket: ws://127.0.0.1:5180/hdxrc.
  • Transport: UTF-8 WebSocket text messages.
  • One WebSocket message corresponds to one complete JSON document.
  • Maximum size: 1.048.576 bytes UTF-8 (1 MiB).
  • The Gateway supports WebSocket fragmentation and reassembles up to EndOfMessage.
  • Line breaks are not used as delimiters.
  • Message order within a WebSocket connection is preserved.

For another machine, a URL can be configured ws:// o wss://. The default installation listens only on loopback and does not provide TLS. Network exposure must include WSS, a firewall, and infrastructure authentication.

Useful HTTP endpoints:

  • GET /: service information.
  • GET /health: { "status": "ok", "pipeConnected": true|false, "websocketClients": N }.
  • GET /ready: HTTP 200 if HDX is connected to the Gateway; 503 if it is not.

/ready is useful for diagnostics, but the App must also monitor the messages connection.changed and the field hdxConnected of its session.

3. WebSocket Client Requirements

The client implementation must:

  1. accept text messages only;
  2. reensamblar frames fragmentados;
  3. validar UTF-8;
  4. reject or ignore messages larger than 1 MiB;
  5. serialize its own concurrent writes;
  6. keep a receive loop active throughout the session;
  7. generate a requestId unique and non-empty for each message sent;
  8. tolerate additional fields, new events, and unknown types;
  9. not log passwords or complete authentication messages.

4. Required Connection Sequence

The correct order is:

Conectar WebSocket
  → enviar ECP `session.hello`
  ← recibir ECP `session.state`
  ← opcionalmente session.setup
  → enviar ECP `command.execute/authenticate`
  ← recibir ECP `command.result/applied`
  → pedir snapshots y suscripciones
  → si se operará: control.request
  ← esperar control.granted
  → enviar acciones mutantes
  → el control permanece hasta transferencia, liberación o desconexión

A HARDATA ECP action must not be sent before session.helloThe Gateway requires session.hello within the first 10 seconds or it closes the socket with PolicyViolation.

5. Session, Authorization, and Control

5.1 Envelope

{
  "ecp": {
    "version": "1.0",
    "type": "session.hello",
    "requestId": "gw-client-001"
  },
  "data": {}
}

Rules for the App:

  • always send version = "1.0";
  • use a requestId unique value for each ECP operation;
  • tratar data as an object;
  • correlate responses when they repeat the requestId;
  • accept spontaneous events with requestId = null.

5.2 session.hello

It must be the first application message:

{
  "ecp": {
    "version": "1.0",
    "type": "session.hello",
    "requestId": "hello-001"
  },
  "data": {
    "clientId": "app-pc-estudio-01",
    "clientName": "Aplicación externa",
    "applicationType": "operator-ui",
    "applicationId": "<id-estable-de-aplicación>",
    "applicationKey": "<secreto aleatorio persistente>",
    "machineName": "ESTUDIO-01",
    "requestedRole": "observer"
  }
}

Fields:

Field Type Requisito Semantics
clientId string recomendado Stable instance identity. If omitted, the Gateway uses the session GUID.
clientName string recomendado Human-readable name. If omitted, it uses clientId.
applicationType string recomendado App type, for example operator-ui, automation u observer.
applicationId string for control Stable installation identity.
applicationKey string for control Persistent random secret of at least 256 bits.
machineName string recomendado Machine shown by HDX during approval.
requestedRole string yes controller u observer. Any value other than controller is interpreted as observer.

HARDATA ECP 1.0 utiliza exclusivamente applicationType. There are no public aliases for this field.

Response: ecp.type = "session.state".

5.3 Session State

Example of session.state o control.granted:

{
  "ecp": {
    "version": "1.0",
    "type": "session.state",
    "requestId": "hello-001"
  },
  "data": {
    "sessionId": "55ef054f-1708-4787-ab7f-185c739be002",
    "clientId": "app-pc-estudio-01",
    "requestedRole": "observer",
    "grantedRole": "observer",
    "controlState": "authenticationRequired",
    "applicationAuthorization": "pending",
    "hdxConnected": true,
    "remoteControlAvailable": true,
    "remoteControlArmed": false,
    "connectionEpoch": "d9fdb740-5818-436a-99e5-8745fbc5a31e",
    "controller": null,
    "authenticated": false
  }
}

Values of controlState:

  • authenticationRequired: the session must authenticate.
  • active: the session owns control.
  • available: HDX is armed and the session may request control.
  • waitingForHdxArm: HDX has not yet enabled remote operation.
  • hdxProtocolUnavailable: the Gateway is connected but does not yet have a valid session with HDX.

controller, when it is not null, contains:

{
  "sessionId": "...",
  "clientId": "otra-app",
  "clientName": "Operador principal",
  "applicationType": "operator-ui"
}

5.4 Request Control

{
  "ecp": {
    "version": "1.0",
    "type": "control.request",
    "requestId": "control-001"
  },
  "data": {}
}

Successful response: control.granted. Negative response: control.denied.

The session must be authenticated, approved by HDX, and HDX must report remoteControlArmed = true. Only one controller exists at a time. A valid request transfers control: the most recent authorized request wins.

5.5 Control Duration

There is no application lease or heartbeat. Control remains associated with the session until another approved App requests it, the App releases it, the connection is lost, HDX is no longer armed, or authorization is revoked.

HDX keeps all known identities, including rejected ones. An unknown identity generates a single prompt to the operator; rejected and revoked are remembered and do not generate further requests. The persistent application list can be managed by the operator from the HDX remote control properties.

5.6 Release Control

{
  "ecp": {
    "version": "1.0",
    "type": "control.release",
    "requestId": "release-001"
  },
  "data": {}
}

The session returns to observer and receives session.state.

5.7 Eventos Gateway

ecp.type Destinatario Main content
session.state session Updated state.
control.granted session Control granted.
control.denied session reason and active controller.
control.revoked session reason.
control.changed todas controller, reason.
session.authorizationChanged session Persistent state decided by HDX.
connection.changed todas hdxConnected, connectionEpoch.
error session code and message.

Reasons for control.denied:

  • authenticationRequired
  • waitingForHdxArm
  • authorizationPending
  • applicationRejected
  • authorizationRevoked
  • notController

Observable revocation/change reasons:

  • replaced
  • released
  • disconnected
  • hdxDisconnected
  • hdxNotArmed
  • control.granted

6. Commands, Results, and Errors

6.1 Action Envelope

{
  "ecp": {
    "type": "command.execute",
    "version": "1.0",
    "requestId": "9b228692-203a-41bf-8871-ad2810c7be7a"
  },
  "data": {
    "windowId": "musicWindow",
    "action": "getWindowSnapshot",
    "params": {}
  }
}

Requisitos:

  • ecp.type: command.execute.
  • ecp.version: string exacto 1.0.
  • ecp.requestId: non-empty string unique among pending requests.
  • data.windowId: window identifier.
  • data.action: action name.
  • data.params: JSON object, even when empty.

Do not use data.value, legacy envelopes, or parameters packed into strings.

Windows recognized by Gateway subscriptions:

  • adsWindow
  • musicWindow
  • auxWindow

Global actions use windowId = "global".

6.2 Action Result

{
  "ecp": {
    "type": "command.result",
    "version": "1.0",
    "requestId": "9b228692-203a-41bf-8871-ad2810c7be7a",
    "sequence": 1203,
    "timestampMonotonicMs": 92350000
  },
  "data": {
    "action": "play",
    "status": "applied",
    "windowId": "musicWindow",
    "playlistRevision": 81
  }
}

Possible states:

  • accepted: accepted but not yet terminal; keep the request pending.
  • applied: applied; successful terminal result.
  • rejected: rejected; failed terminal result.

Not all responses contain playlistRevision. The App must tolerate its absence.

6.3 Error HARDATA ECP

{
  "ecp": {
    "type": "error",
    "version": "1.0",
    "requestId": "9b228692-203a-41bf-8871-ad2810c7be7a",
    "sequence": 1204,
    "timestampMonotonicMs": 92350001
  },
  "data": {
    "windowId": "musicWindow",
    "action": "setActualById",
    "code": "invalidParameters",
    "message": "setActualById requires playlistItemId"
  }
}

Un error is terminal. The canonical public fields are data.code and data.messageAn App must not depend on an additional error object. The Gateway completes correlation with the first terminal response and suppresses subsequent duplicate private responses.

7. HDX Authentication

Send after receiving session.setup:

{
  "ecp": {
    "type": "command.execute",
    "version": "1.0",
    "requestId": "auth-001"
  },
  "data": {
    "windowId": "global",
    "action": "authenticate",
    "params": {
      "username": "<usuario>",
      "password": "<contraseña>"
    }
  }
}

Authentication is successful only upon receiving command.result with the same requestId and data.status = "applied". Do not infer success from the connection or from session.setup.

Possible errors:

  • invalidCredentials
  • authLocked
  • remoteControlDisabled

HDX counts failed attempts on the shared Pipe connection. Avoid aggressive automatic retries. Request new credentials from the user after a rejection.

When session.state.controlState is authenticationRequired or when connection.changed with hdxConnected = false:

  1. mark the session as unauthenticated;
  2. stop sending actions;
  3. discard state dependent on the connectionEpoch anterior;
  4. wait for reconnection;
  5. autenticar nuevamente;
  6. request snapshots, playlist, and subscriptions again;
  7. request control again if applicable.

8. Implemented Actions and Parameters

8.1 Actions That Do Not Require Control

The Gateway treats the following as read-only or locally managed:

Action windowId params Result
authenticate global username, password Validated by HDX.
getWindowSnapshot window {} HDX emite snapshot.window.
getPlaylist window {} HDX emite snapshot.playlist.
setPlayerStateSubscription window rateMs Consumed and aggregated by the Gateway.
clearPlayerStateSubscription window {} Consumed by the Gateway.

setPlayerStateSubscription and clearPlayerStateSubscription respond locally with command.result/applied and are not forwarded to HDX. The App's complete identity is declared only once in session.hello.

8.2 Playback and State

These actions require control:

Action Typed Parameters Semantics
play {} Starts or resumes playback.
pause {} Pauses playback.
stop {} Stops playback.
fadeOut {} Requests a fade of the current playback. Subsequent behavior depends on the local HDX state.
airCue {} Executes the window Air Cue command.
setPositionMs { "positionMs": 42850 } Seeks the current media item to a position in milliseconds.
setActualById { "playlistItemId": "456", "expectedPlaylistRevision": 81 } Sets current by playlist identity. The revision is optional and the current runtime does not validate it.
setNextById same as above Sets next by identity.
selectForPlayById { "playlistItemId": "456" } Selects the item for playback.
setTalkOver { "enabled": true } Idempotent talk-over state.
setStopAtEnd { "enabled": true } Idempotent stop-at-end state.
mainVolume { "volume": 70 } Sets the main volume.
auxVolume { "volume": 70 } Define volumen auxiliar.
clearList {} Clears the window playlist.
toggleAscendingTimer { "enabled": true } Configura timer ascendente.

In setTalkOver and setStopAtEnd, enabled is required and boolean. These are absolute assignments, not toggles: repeating enabled: true keeps the feature enabled, and repeating enabled: false keeps it disabled. The Gateway rejects missing values or values of another type with invalidParams; retries and duplicate messages must not invert the state in HDX.

Use playlistItemId as an opaque string even if some values look numeric. Do not use index as persistent identity.

8.3 Playlist Editing

All require control:

Action Parameters
selectItems { "playlistItemIds": "101,102,103" }
deleteSelected { "playlistItemIds": "101,102,103" }
copySelected { "playlistItemIds": "101,102,103" }
pasteClipboard { "anchorPlaylistItemId": "105" }; the anchor is optional.
moveItemBefore { "playlistItemId": "103", "beforePlaylistItemId": "101" }; destination optional depending on the operation.
insertMaterial { "anchorPlaylistItemId": "105", "materialId": "9001" }
insertOverlay { "anchorPlaylistItemId": "105", "materialId": "9002", "offsetSeconds": 3 }; offset opcional.
insertBlock { "title": "Bloque", "time": "12:30:00" }

The current implementation transports playlistItemIds as a comma-separated string, not as a JSON array.

8.4 Extensibilidad

The public contract includes only the actions documented in this section. An App must not invent action names or depend on undocumented internal behavior. Future extensions will be documented and versioned within HARDATA ECP.

9. playerStatus Subscription

Alta:

{
  "ecp": {
    "type": "command.execute",
    "version": "1.0",
    "requestId": "sub-music-001"
  },
  "data": {
    "windowId": "musicWindow",
    "action": "setPlayerStateSubscription",
    "params": { "rateMs": 500 }
  }
}

Unsubscribe: same structure with action = "clearPlayerStateSubscription" and params = {}.

Reglas:

  • The Gateway applies a per-App clamp of 100 a 10000 ms.
  • If omitted rateMs, it uses 500 ms.
  • It only adds subscriptions for authenticated Apps.
  • Toward HDX, it requests the fastest cadence required by all Apps.
  • Each App receives playerStatus at most according to its own cadence.
  • Without a subscription for a window, the App does not receive playerStatus from that window.
  • Other public events do not depend on this subscription.
  • HDX may emit faster during transitions; the Gateway still applies the App-specific limit.

10. HARDATA ECP Events and Snapshots

10.1 State Envelope

{
  "ecp": {
    "version": "1.0",
    "type": "state.playbackStateChanged",
    "sequence": 991204,
    "timestampMonotonicMs": 92345678
  },
  "data": {}
}

sequence is monotonic per window for states emitted by HDX. timestampMonotonicMs is monotonic time from the HDX process and is not a UTC date. The App must store lastSequence per window and discard a state if sequence <= lastSequence.

The global remoteControlStateChanged event currently includes sequence but may omit timestampMonotonicMs; clients must tolerate its absence.

10.2 remoteControlStateChanged

{
  "ecp": {
    "version": "1.0",
    "type": "state.remoteControlStateChanged",
    "sequence": 20
  },
  "data": {
    "remoteControlAvailable": true,
    "remoteControlArmed": true
  }
}

remoteControlArmed is the effective condition for the Gateway to grant and retain control. Automatic modes belong exclusively to local HDX operation: ECP neither exposes them nor allows them to be modified. Internal actions autoMode and setAutoEnabled return actionNotAvailable if an App attempts to send them.

EXT CTRL is only the current label of the HDX button that used to be called RC. It is not a HARDATA ECP mode, state, command, or value. For HDX to report remoteControlArmed = true, the operator must press that button and keep AUTO; both operations are performed locally in HDX.

10.3 playbackStateChanged

{
  "ecp": {
    "version": "1.0",
    "type": "state.playbackStateChanged",
    "sequence": 101,
    "timestampMonotonicMs": 45000
  },
  "data": {
    "windowId": "musicWindow",
    "playbackState": "playing",
    "talkOverEnabled": false,
    "stopAtEndEnabled": false,
    "mainVolume": 100,
    "auxVolume": 70
  }
}

Expected playback states: stopped, playing, paused. Tolerate future values.

10.4 actualNextChanged

{
  "ecp": {
    "version": "1.0",
    "type": "state.actualNextChanged",
    "sequence": 102,
    "timestampMonotonicMs": 45100
  },
  "data": {
    "windowId": "musicWindow",
    "actualPlaylistItemId": "456",
    "nextPlaylistItemId": "489"
  }
}

Both identifiers may be null.

10.5 playerStatus

{
  "ecp": {
    "version": "1.0",
    "type": "state.playerStatus",
    "sequence": 103,
    "timestampMonotonicMs": 45200
  },
  "data": {
    "windowId": "musicWindow",
    "onAirTimer": "00:12:34",
    "itemTimer": "00:01:28",
    "playlistItemId": "456",
    "materialId": "123",
    "positionMs": 42850,
    "durationMs": 215000,
    "remainingMs": 172150,
    "phase": "intro",
    "playbackState": "playing",
    "actualPlaylistItemId": "456",
    "nextPlaylistItemId": "489",
    "vuLeft": 73,
    "vuRight": 69
  }
}

All four identifiers may be strings or null. positionMs, durationMs and remainingMs are the canonical time reference. Formatted timers are for presentation only. phase may change as the player evolves; do not use a closed enumeration.

10.6 snapshot.window

Response to getWindowSnapshot:

{
  "ecp": {
    "version": "1.0",
    "type": "snapshot.window",
    "sequence": 104,
    "timestampMonotonicMs": 45300
  },
  "data": {
    "windowId": "musicWindow",
    "playbackState": "playing",
    "talkOverEnabled": false,
    "stopAtEndEnabled": false,
    "mainVolume": 100,
    "auxVolume": 70,
    "actualPlaylistItemId": "456",
    "nextPlaylistItemId": "489",
    "positionMs": 42850,
    "durationMs": 215000,
    "remainingMs": 172150,
    "playbackPhase": "intro",
    "playlistRevision": 81
  }
}

The snapshot does not increment playlistRevision.

10.7 snapshot.playlist and playlist.updated

The names snapshot.playlist and playlist.updated appear directly in ecp.type:

{
  "ecp": {
    "type": "snapshot.playlist",
    "version": "1.0",
    "sequence": 105,
    "timestampMonotonicMs": 45400
  },
  "data": {
    "schemaVersion": "1.2",
    "windowId": "musicWindow",
    "playlistRevision": 81,
    "actualIndex": 4,
    "nextIndex": 5,
    "nodes": [
      {
        "playlistItemId": "456",
        "materialId": "123",
        "index": 4,
        "nodeType": "material",
        "parentIndex": null,
        "level": 1,
        "childrenCount": 0,
        "relativeStartOffsetSeconds": null,
        "title": "Título",
        "playStatus": "actual",
        "dcs": "",
        "mediaType": "audio",
        "overType": ""
      }
    ]
  }
}

Reglas:

  • snapshot.playlist reports the current list without incrementing the revision.
  • playlist.updated represents a publication after a structural mutation and increments the revision.
  • actualIndex, nextIndex, parentIndex, relativeStartOffsetSeconds, playlistItemId and materialId pueden ser null.
  • playlistItemId identifies a specific instance within the playlist; it is used to select, move, or operate on that node through HARDATA ECP.
  • materialId identifies the media item in the HARDATA platform; it is used to query its metadata and related resources through HARDATA API.
  • playlistItemId and materialId are not interchangeable: the same media item may appear multiple times in a playlist, and each occurrence will have its own playlistItemId.
  • Treat both identifiers as opaque strings even if their contents look numeric. Do not infer meaning or construct them from other fields.
  • materialId may be null in blocks, groups, or other nodes that do not represent a media item queryable through HARDATA API.
  • index is only the node's current position and must not be used as persistent identity.
  • A full reload may reassign identifiers.
  • Locally replace the complete playlist with data.nodes when either of the two types is received.
  • Update atomically playlistRevision, punteros and items.

11. session.setup

HDX emits it when the internal handshake completes. The Gateway stores the latest one and delivers it to each session after session.hello, even before authentication:

{
  "ecp": {
    "type": "session.setup",
    "version": "1.0"
  },
  "data": {
    "remoteControlEnabled": true,
    "authRequired": true,
    "station": "Radio Uno",
    "studio": "Aire 1",
    "workstation": "ESTUDIO-A",
    "workstationIP": "192.168.1.50",
    "webRTC": "https://localhost:5100/webrtc",
    "protocolVersion": "1.2",
    "capabilities": [
      "playlistItemId",
      "correlatedCommands",
      "atomicWindowSnapshot",
      "adaptiveTimer"
    ]
  }
}

The App must treat the fields as informational and tolerate missing fields or new capabilities. data.protocolVersion and the fields schemaVersion in other messages describe internal data transported by the Gateway; they are not another version of the public protocol and must not be used to dispatch messages. For that purpose, validation is based exclusively on ecp.version. Receiving session.setup does not mean that the session is authenticated.

12. Distribution, Cache, and Correlation

The Gateway applies these rules:

  • session.setup: for sessions that sent session.hello.
  • Results and errors with requestId: only for the session that originated the request.
  • Public states, snapshots, and playlists: only for authenticated sessions.
  • playerStatus: additionally requires a subscription for the window.

Before forwarding an action to HDX, the Gateway replaces the requestId with an internal one and adds audit information. When the response returns, it restores the requestId original value. This mechanism is transparent to the App.

The Gateway caches the latest public message by connectionEpoch, window and event/type. When an App authenticates, it replays the cache ordered by sequence. Therefore, states may arrive immediately after successful authentication, before responses to snapshots requested by the App.

The App should design its store as an idempotent reducer and must not assume a rigid event sequence after authentication.

13. Errors Generated by the Gateway

All public errors use ecp.type = "error":

{
  "ecp": {
    "version": "1.0",
    "type": "error",
    "requestId": "req-001"
  },
  "data": {
    "code": "controlRequired",
    "message": "Session does not own control"
  }
}
Code Significado Recovery
invalidJson Invalid JSON. Fix serialization.
invalidEnvelope The object is missing ecp. Corregir envelope.
unsupportedVersion ecp.version is not supported. Use a published version accepted by the Gateway.
helloRequired An operation was attempted before session.hello. Send session.hello.
invalidParams Public parameters are missing or have an incorrect type. Corregir data.params.
authenticationRequired The session is not authenticated. Ejecutar authenticate.
controlRequired Mutating action without control. Request control and wait for control.granted.
authorizationPending HDX has not decided yet. Waits forr session.authorizationChanged.
applicationRejected HDX rejected the identity. The operator must change the decision in HDX.
remoteControlNotArmed HDX is not armed for external control. In HDX, press EXT CTRL (previously RC) and enable AUTO; then wait for remoteControlArmed = true.
actionNotAvailable The action belongs to local HDX operation and is not part of ECP. Do not send it from an external App.
hdxUnavailable Pipe disconnected or send failure. Wait for reconnection and reauthenticate.
requestTimeout HDX did not complete within the configured timeout. Mark the result as unknown and resynchronize; do not automatically repeat non-idempotent actions.
unknownMessageType ecp.type desconocido. Correct the type.

Additional observable HDX errors:

  • remoteControlDisabled
  • authRequired
  • invalidCredentials
  • authLocked
  • invalidWindow
  • windowNotOpen
  • invalidParameters
  • notAllowed
  • invalidState
  • invalidTarget
  • invalidPlaylistItemId
  • invalidItemId

The list may grow. Show data.message to the operator and preserve data.code for programmatic logic.

14. Recommended State Machine

DISCONNECTED
  └─ WebSocket abierto → HELLO_SENT
       └─ session.state → UNAUTHENTICATED
            └─ authenticate/applied → OBSERVING
                 ├─ authorization approved → AUTHORIZED
                 │    └─ control.request/control.granted → CONTROLLING
                 │         └─ replaced/released/not armed → OBSERVING
                 └─ desconexión HDX → UNAUTHENTICATED

Cualquier cierre WebSocket → DISCONNECTED

Do not enable mutating controls in the UI except in CONTROLLING. Queries and subscriptions are enabled in OBSERVING and CONTROLLING.

15. Recommended Local State

Maintain at least:

GatewayState
  websocketConnected
  hdxConnected
  connectionEpoch
  authenticated
  requestedRole
  grantedRole
  ownsControl
  applicationAuthorization
  remoteControlAvailable
  remoteControlArmed
  controller

WindowState por windowId
  lastSequence
  playbackState
  talkOverEnabled
  stopAtEndEnabled
  mainVolume
  auxVolume
  actualPlaylistItemId
  nextPlaylistItemId
  positionMs
  durationMs
  remainingMs
  phase
  vuLeft
  vuRight
  playlistRevision
  playlist[]

PendingRequest por requestId
  action
  windowId
  sentAt
  idempotent

When changing connectionEpoch, reiniciar lastSequence, pending requests, authentication, control, and states whose validity depends on the previous HDX connection.

16. Minimum Implementation Algorithm

Pseudocode:

connect(url):
  abrir WebSocket
  iniciar receiveLoop
  send("session.hello", identity)

onMessage(json):
  if json.ecp.version != "1.0":
    log and ignore
    return

  switch json.ecp.type:
    case "session.state", "session.authorizationChanged": update session
    case "connection.changed": reset state if connectionEpoch changed
    case "control.granted": ownsControl = true
    case "control.denied", "control.revoked": ownsControl = false
    case "command.result": correlate; finish unless status == "accepted"
    case "error": correlate and finish as failure
    case "snapshot.window": replace window snapshot
    case "snapshot.playlist", "playlist.updated": replace playlist atomically
    case type starts with "state.": apply event if sequence is newer
    default: log and ignore

sendAction(window, action, params):
  require authenticated
  if action is mutating: require ownsControl
  requestId = new UUID
  store pending request
  send text JSON with HARDATA ECP 1.0 envelope

17. Recommended Startup After Authentication

For each window the App will display:

  1. getWindowSnapshot.
  2. getPlaylist.
  3. setPlayerStateSubscription with rateMs = 500.

For a controlling App:

  1. instruct the operator to press EXT CTRL and enable AUTO if they are not already active;
  2. wait for remoteControlArmed = true;
  3. send control.request;
  4. wait for control.granted;
  5. verify that HDX reported authorization approved;
  6. enable mutating controls.

It is not necessary to wait sequentially for the first three results before processing events. Correlate each request independently.

18. Safe Reconnection

When the WebSocket closes:

  • mark control and authentication as lost;
  • mark pending requests as having unknown outcome;
  • reconnect with backoff, for example 1, 2, 5, 10 seconds with jitter;
  • restart from session.hello.

After a requestTimeout, do not automatically repeat mutating actions such as insertMaterial, deleteSelected, moveItemBefore or playback changes. The action may have been applied even if its response was lost. First request snapshot.window and snapshot.playlist to reconcile.

Idempotent actions (setTalkOver, setStopAtEnd and absolute volume/position values) may be retried only after resynchronizing and regaining control.

19. Seguridad

  • Do not store or log passwords in plain text.
  • Send credentials only over loopback or trusted WSS.
  • Do not expose the internal Gateway-HDX token; an App must never use gatewayHello.
  • Validate the certificate when using WSS.
  • Limit size before serializing/sending.
  • Escape visible content such as titles; do not interpret received HTML.
  • Treat names, titles, and error messages as untrusted data.
  • Separate observation permissions from control permissions in the UI.

20. App Acceptance Checklist

Transporte

  • Connects through ws/wss and uses UTF-8 text.
  • Reassembles fragments and applies a 1 MiB limit.
  • Serializes concurrent sends.

Session

  • Sends session.hello inmediatamente.
  • Processes all known HARDATA ECP 1.0 types.
  • Authenticates each new connection/epoch.
  • Does not confuse session.setup with successful authentication.

Control

  • Waits for control.granted before sending mutating actions.
  • Waits for authorization approved before requesting control.
  • Disables operation on revocation, disarming, or disconnection.
  • Releases control during orderly shutdown.

HARDATA ECP

  • Uses ecp.version = "1.0" and params tipados.
  • Genera requestId unique.
  • Correlates command.result, error, and Gateway errors.
  • Mantiene accepted pending until a terminal result.
  • Uses playlistItemId as identity and not index.
  • Uses materialId —when present—to query the media item through HARDATA API.
  • Does not confuse materialId with playlistItemId.
  • Discards old sequences per window.
  • Replaces the playlist atomically.
  • Tolerates nulls, missing fields, and extensions.

Recovery

  • Resets state when changing connectionEpoch.
  • Rebuilds snapshots and subscriptions after reconnecting.
  • Does not repeat non-idempotent mutations after a timeout without reconciling.

21. Complete Example Flow

21.1 Identification

{"ecp":{"version":"1.0","type":"session.hello","requestId":"g1"},"data":{"clientId":"demo-1","clientName":"Demo","applicationType":"operator-ui","requestedRole":"controller"}}

21.2 Authentication

{"ecp":{"type":"command.execute","version":"1.0","requestId":"a1"},"data":{"windowId":"global","action":"authenticate","params":{"username":"<usuario>","password":"<contraseña>"}}}

Wait for:

{"ecp":{"type":"command.result","version":"1.0","requestId":"a1"},"data":{"action":"authenticate","status":"applied","windowId":"global"}}

21.3 Synchronization

{"ecp":{"type":"command.execute","version":"1.0","requestId":"s1"},"data":{"windowId":"musicWindow","action":"getWindowSnapshot","params":{}}}
{"ecp":{"type":"command.execute","version":"1.0","requestId":"s2"},"data":{"windowId":"musicWindow","action":"getPlaylist","params":{}}}
{"ecp":{"type":"command.execute","version":"1.0","requestId":"s3"},"data":{"windowId":"musicWindow","action":"setPlayerStateSubscription","params":{"rateMs":500}}}

21.4 Control

{"ecp":{"version":"1.0","type":"control.request","requestId":"g2"},"data":{}}

Only after control.granted:

{"ecp":{"type":"command.execute","version":"1.0","requestId":"p1"},"data":{"windowId":"musicWindow","action":"play","params":{}}}

21.5 Transferencia

Another approved App may send control.request. The previous controller receives control.revoked with reason replaced; there is no application heartbeat.


With this contract, an App can connect, authenticate, observe state, maintain playlists, acquire exclusive control, execute actions, and correctly recover from disconnections without knowledge of the internal Gateway or HDX Radio code.