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.576bytes 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: HTTP200if HDX is connected to the Gateway;503if 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:
- accept text messages only;
- reensamblar frames fragmentados;
- validar UTF-8;
- reject or ignore messages larger than 1 MiB;
- serialize its own concurrent writes;
- keep a receive loop active throughout the session;
- generate a
requestIdunique and non-empty for each message sent; - tolerate additional fields, new events, and unknown types;
- 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
requestIdunique value for each ECP operation; - tratar
dataas 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:
authenticationRequiredwaitingForHdxArmauthorizationPendingapplicationRejectedauthorizationRevokednotController
Observable revocation/change reasons:
replacedreleaseddisconnectedhdxDisconnectedhdxNotArmedcontrol.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 exacto1.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:
adsWindowmusicWindowauxWindow
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:
invalidCredentialsauthLockedremoteControlDisabled
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:
- mark the session as unauthenticated;
- stop sending actions;
- discard state dependent on the
connectionEpochanterior; - wait for reconnection;
- autenticar nuevamente;
- request snapshots, playlist, and subscriptions again;
- 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
100a10000 ms. - If omitted
rateMs, it uses500 ms. - It only adds subscriptions for authenticated Apps.
- Toward HDX, it requests the fastest cadence required by all Apps.
- Each App receives
playerStatusat most according to its own cadence. - Without a subscription for a window, the App does not receive
playerStatusfrom 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.playlistreports the current list without incrementing the revision.playlist.updatedrepresents a publication after a structural mutation and increments the revision.actualIndex,nextIndex,parentIndex,relativeStartOffsetSeconds,playlistItemIdandmaterialIdpueden sernull.playlistItemIdidentifies a specific instance within the playlist; it is used to select, move, or operate on that node through HARDATA ECP.materialIdidentifies the media item in the HARDATA platform; it is used to query its metadata and related resources through HARDATA API.playlistItemIdandmaterialIdare not interchangeable: the same media item may appear multiple times in a playlist, and each occurrence will have its ownplaylistItemId.- Treat both identifiers as opaque strings even if their contents look numeric. Do not infer meaning or construct them from other fields.
materialIdmay benullin blocks, groups, or other nodes that do not represent a media item queryable through HARDATA API.indexis 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.nodeswhen 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 sentsession.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:
remoteControlDisabledauthRequiredinvalidCredentialsauthLockedinvalidWindowwindowNotOpeninvalidParametersnotAllowedinvalidStateinvalidTargetinvalidPlaylistItemIdinvalidItemId
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:
getWindowSnapshot.getPlaylist.setPlayerStateSubscriptionwithrateMs = 500.
For a controlling App:
- instruct the operator to press
EXT CTRLand enableAUTOif they are not already active; - wait for
remoteControlArmed = true; - send
control.request; - wait for
control.granted; - verify that HDX reported authorization
approved; - 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/wssand uses UTF-8 text. - Reassembles fragments and applies a 1 MiB limit.
- Serializes concurrent sends.
Session
- Sends
session.helloinmediatamente. - Processes all known HARDATA ECP 1.0 types.
- Authenticates each new connection/epoch.
- Does not confuse
session.setupwith successful authentication.
Control
- Waits for
control.grantedbefore sending mutating actions. - Waits for authorization
approvedbefore requesting control. - Disables operation on revocation, disarming, or disconnection.
- Releases control during orderly shutdown.
HARDATA ECP
- Uses
ecp.version = "1.0"andparamstipados. - Genera
requestIdunique. - Correlates
command.result,error, and Gateway errors. - Mantiene
acceptedpending until a terminal result. - Uses
playlistItemIdas identity and notindex. - Uses
materialId—when present—to query the media item through HARDATA API. - Does not confuse
materialIdwithplaylistItemId. - 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.