Herramientas de Integración

Hardata ECP (External Control Protocol)

Versión del documento: 1.0
Contrato público cubierto: HARDATA ECP 1.0

Estado de referencia: implementación disponible al 19 de agosto de 2026

Documento público y autocontenido para desarrolladores de aplicaciones externas de control HARDATA. Los nombres de campos, versiones y envelopes que aparecen aquí forman parte del contrato de interoperabilidad y deben respetarse exactamente.

Importante: HARDATA ECP controla HARDATA HDX 5 ó DINESAT 13 en tiempo real mediante WebSocket. Para consultar o administrar metadata, materiales y tráfico publicitario mediante HTTP use la especificación separada HARDATA API.

Este documento es autocontenido. Una persona o una IA debe poder desarrollar una App cliente funcional sin consultar el código fuente ni documentación adicional.

1. Objetivo y modelo mental

La App no se conecta directamente a HDX Radio. Se conecta por WebSocket al Gateway local. El Gateway mantiene una única conexión Named Pipe con HDX Radio, autentica cada sesión lógica, arbitra quién puede operar, correlaciona respuestas y distribuye estados.

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 es el único protocolo público. Todos los mensajes usan el objeto raíz ecp; la App no conoce ni interpreta el protocolo privado entre Gateway y HDX.

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

El Gateway resuelve sesión, autorización, control y traducción de comandos, respuestas y eventos. Esta separación permite evolucionar HDX sin trasladar esa complejidad a las aplicaciones clientes.

HARDATA ECP 1.0 es la primera versión del contrato público. El contrato utiliza exclusivamente el envelope ecp y los nombres de campos definidos en este documento; no existen envelopes, aliases ni acciones públicas heredadas. El protocolo HDXRC 1.2 continúa existiendo sólo como detalle interno entre Gateway y HDX.

2. Dirección, transporte y límites

Valores predeterminados:

  • WebSocket: ws://127.0.0.1:5180/hdxrc.
  • Transporte: mensajes WebSocket de texto UTF-8.
  • Un mensaje WebSocket equivale a un documento JSON completo.
  • Tamaño máximo: 1.048.576 bytes UTF-8 (1 MiB).
  • El Gateway admite fragmentación WebSocket y reensambla hasta EndOfMessage.
  • No se usan saltos de línea como delimitadores.
  • El orden de mensajes de una conexión WebSocket se conserva.

Para otra máquina se puede configurar una URL ws:// o wss://. La instalación predeterminada escucha solo en loopback y no aporta TLS. Una publicación en red debe incorporar WSS, firewall y autenticación de infraestructura.

Endpoints HTTP útiles:

  • GET /: información del servicio.
  • GET /health: { "status": "ok", "pipeConnected": true|false, "websocketClients": N }.
  • GET /ready: HTTP 200 si HDX está conectado al Gateway; 503 si no lo está.

/ready sirve para diagnóstico, pero la App debe observar también los mensajes connection.changed y el campo hdxConnected de su sesión.

3. Requisitos del cliente WebSocket

La implementación cliente debe:

  1. aceptar solamente mensajes de texto;
  2. reensamblar frames fragmentados;
  3. validar UTF-8;
  4. rechazar o ignorar mensajes mayores a 1 MiB;
  5. serializar sus propias escrituras concurrentes;
  6. conservar un bucle de recepción activo durante toda la sesión;
  7. generar un requestId único y no vacío para cada mensaje enviado;
  8. tolerar campos adicionales, eventos nuevos y tipos desconocidos;
  9. no registrar contraseñas ni mensajes de autenticación completos.

4. Secuencia de conexión obligatoria

El orden correcto es:

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

No se debe enviar una acción HARDATA ECP antes de session.hello. El Gateway exige session.hello dentro de los primeros 10 segundos o cierra el socket con PolicyViolation.

5. Sesión, autorización y control

5.1 Envelope

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

Reglas para la App:

  • enviar siempre version = "1.0";
  • usar un requestId único por operación ECP;
  • tratar data como objeto;
  • correlacionar respuestas cuando repitan el requestId;
  • aceptar eventos espontáneos con requestId = null.

5.2 session.hello

Debe ser el primer mensaje de aplicación:

{
  "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"
  }
}

Campos:

Campo Tipo Requisito Semántica
clientId string recomendado Identidad estable de la instancia. Si falta, el Gateway usa el GUID de sesión.
clientName string recomendado Nombre legible. Si falta, usa clientId.
applicationType string recomendado Tipo de App, por ejemplo operator-ui, automation u observer.
applicationId string para controlar Identidad estable de la instalación.
applicationKey string para controlar Secreto aleatorio persistente de al menos 256 bits.
machineName string recomendado Equipo mostrado por HDX durante la aprobación.
requestedRole string sí controller u observer. Cualquier valor distinto de controller se interpreta como observer.

HARDATA ECP 1.0 utiliza exclusivamente applicationType. No existen aliases públicos para este campo.

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

5.3 Estado de sesión

Ejemplo de 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
  }
}

Valores de controlState:

  • authenticationRequired: la sesión debe autenticarse.
  • active: la sesión posee el control.
  • available: HDX está armado y la sesión puede pedir control.
  • waitingForHdxArm: HDX todavía no habilita operación remota.
  • hdxProtocolUnavailable: el Gateway está conectado, pero todavía no dispone de una sesión válida con HDX.

controller, cuando no es null, contiene:

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

5.4 Pedir control

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

Respuesta exitosa: control.granted. Respuesta negativa: control.denied.

La sesión debe estar autenticada, aprobada por HDX y HDX debe informar remoteControlArmed = true. Solo existe un controlador a la vez. Una solicitud válida transfiere el control: la última solicitud autorizada gana.

5.5 Duración del control

No existe lease ni heartbeat de aplicación. El control permanece asociado a la sesión hasta que otra App aprobada lo solicita, la App lo libera, se pierde la conexión, HDX deja de estar armado o se revoca la autorización.

HDX mantiene todas las identidades conocidas, incluidas las rechazadas. Una identidad desconocida genera una sola consulta al operador; rejected y revoked se recuerdan y no vuelven a producir solicitudes. La lista persistente de aplicaciones puede ser administrada por el operador desde las propiedades de control remoto de HDX.

5.6 Liberar control

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

La sesión vuelve a observer y recibe session.state.

5.7 Eventos Gateway

ecp.type Destinatario Contenido principal
session.state sesión Estado actualizado.
control.granted sesión Control concedido.
control.denied sesión reason y controlador activo.
control.revoked sesión reason.
control.changed todas controller, reason.
session.authorizationChanged sesión Estado persistente decidido por HDX.
connection.changed todas hdxConnected, connectionEpoch.
error sesión code y message.

Razones de control.denied:

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

Razones de revocación/cambio observables:

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

6. Comandos, resultados y errores

6.1 Envelope de acción

{
  "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: string no vacío y único entre requests pendientes.
  • data.windowId: identificador de ventana.
  • data.action: nombre de acción.
  • data.params: objeto JSON, incluso cuando esté vacío.

No usar data.value, envelopes antiguos ni parámetros compactados en strings.

Ventanas reconocidas por las suscripciones del Gateway:

  • adsWindow
  • musicWindow
  • auxWindow

Las acciones globales usan windowId = "global".

6.2 Resultado de acción

{
  "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
  }
}

Estados posibles:

  • accepted: aceptado pero todavía no terminal; mantener el request pendiente.
  • applied: aplicado; resultado terminal exitoso.
  • rejected: rechazado; resultado terminal fallido.

No todas las respuestas contienen playlistRevision. La App debe tolerar su ausencia.

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 es terminal. Los campos públicos canónicos son data.code y data.message; una App no debe depender de un objeto error adicional. El Gateway finaliza la correlación con la primera respuesta terminal y suprime respuestas privadas duplicadas posteriores.

7. Autenticación HDX

Enviar después de recibir session.setup:

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

La autenticación solo es exitosa al recibir command.result con el mismo requestId y data.status = "applied". No inferir éxito por la conexión ni por session.setup.

Errores posibles:

  • invalidCredentials
  • authLocked
  • remoteControlDisabled

HDX cuenta intentos fallidos sobre la conexión Pipe compartida. Evitar reintentos automáticos agresivos. Solicitar nuevas credenciales al usuario después de un rechazo.

Cuando session.state.controlState vale authenticationRequired o llega connection.changed con hdxConnected = false:

  1. marcar la sesión como no autenticada;
  2. dejar de enviar acciones;
  3. descartar estado dependiente del connectionEpoch anterior;
  4. esperar reconexión;
  5. autenticar nuevamente;
  6. volver a pedir snapshots, playlist y suscripciones;
  7. solicitar control nuevamente si corresponde.

8. Acciones y parámetros implementados

8.1 Acciones que no requieren control

El Gateway considera de solo lectura o gestión local:

Acción windowId params Resultado
authenticate global username, password Validado por HDX.
getWindowSnapshot ventana {} HDX emite snapshot.window.
getPlaylist ventana {} HDX emite snapshot.playlist.
setPlayerStateSubscription ventana rateMs Consumida y agregada por Gateway.
clearPlayerStateSubscription ventana {} Consumida por Gateway.

setPlayerStateSubscription y clearPlayerStateSubscription responden localmente con command.result/applied y no se reenvían a HDX. La identidad completa de la App se declara una sola vez en session.hello.

8.2 Reproducción y estado

Estas acciones requieren control:

Acción Parámetros tipados Semántica
play {} Inicia o continúa reproducción.
pause {} Pausa reproducción.
stop {} Detiene reproducción.
fadeOut {} Solicita el fade de la reproducción actual. El comportamiento posterior depende del estado local de HDX.
airCue {} Ejecuta la orden Air Cue de la ventana.
setPositionMs { "positionMs": 42850 } Posiciona el material actual en milisegundos.
setActualById { "playlistItemId": "456", "expectedPlaylistRevision": 81 } Establece actual por identidad de playlist. La revisión es opcional y el runtime actual no la valida.
setNextById igual al anterior Establece siguiente por identidad.
selectForPlayById { "playlistItemId": "456" } Selecciona el item para reproducción.
setTalkOver { "enabled": true } Estado idempotente de talk-over.
setStopAtEnd { "enabled": true } Estado idempotente de detener al final.
mainVolume { "volume": 70 } Define el volumen principal.
auxVolume { "volume": 70 } Define volumen auxiliar.
clearList {} Vacía la playlist de la ventana.
toggleAscendingTimer { "enabled": true } Configura timer ascendente.

En setTalkOver y setStopAtEnd, enabled es obligatorio y booleano. Son asignaciones absolutas, no toggles: repetir enabled: true conserva la función activada y repetir enabled: false la conserva desactivada. El Gateway rechaza valores ausentes o de otro tipo con invalidParams; los reintentos y mensajes duplicados no deben invertir el estado en HDX.

Usar playlistItemId como string opaco aunque algunos valores parezcan números. No usar index como identidad persistente.

8.3 Edición de playlist

Todas requieren control:

Acción Parámetros
selectItems { "playlistItemIds": "101,102,103" }
deleteSelected { "playlistItemIds": "101,102,103" }
copySelected { "playlistItemIds": "101,102,103" }
pasteClipboard { "anchorPlaylistItemId": "105" }; el ancla es opcional.
moveItemBefore { "playlistItemId": "103", "beforePlaylistItemId": "101" }; destino opcional según operación.
insertMaterial { "anchorPlaylistItemId": "105", "materialId": "9001" }
insertOverlay { "anchorPlaylistItemId": "105", "materialId": "9002", "offsetSeconds": 3 }; offset opcional.
insertBlock { "title": "Bloque", "time": "12:30:00" }

La implementación actual transporta playlistItemIds como una cadena separada por comas, no como array JSON.

8.4 Extensibilidad

El contrato público comprende únicamente las acciones documentadas en esta sección. Una App no debe inventar nombres de acción ni depender de comportamientos internos no documentados. Las extensiones futuras se documentarán y versionarán dentro de HARDATA ECP.

9. Suscripción a playerStatus

Alta:

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

Baja: misma estructura con action = "clearPlayerStateSubscription" y params = {}.

Reglas:

  • El Gateway aplica por App un clamp de 100 a 10000 ms.
  • Si falta rateMs, usa 500 ms.
  • Solo agrega suscripciones de Apps autenticadas.
  • Hacia HDX solicita la cadencia más rápida requerida por todas las Apps.
  • Cada App recibe playerStatus como máximo según su propia cadencia.
  • Sin suscripción para una ventana, la App no recibe playerStatus de esa ventana.
  • Los demás eventos públicos no dependen de esta suscripción.
  • HDX puede emitir más rápido en transiciones; el Gateway sigue aplicando el límite individual de la App.

10. Eventos y snapshots HARDATA ECP

10.1 Envelope de estado

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

sequence es monotónica por ventana para los estados emitidos por HDX. timestampMonotonicMs es tiempo monotónico del proceso HDX y no es una fecha UTC. La App debe guardar lastSequence por ventana y descartar un estado si sequence <= lastSequence.

El evento global remoteControlStateChanged actualmente incluye sequence pero puede no incluir timestampMonotonicMs; tolerar su ausencia.

10.2 remoteControlStateChanged

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

remoteControlArmed es la condición efectiva para que el Gateway conceda y conserve el control. Los modos automáticos pertenecen exclusivamente a la operación local de HDX: ECP no los expone ni permite modificarlos. Las acciones internas autoMode y setAutoEnabled reciben actionNotAvailable si una App intenta enviarlas.

EXT CTRL es únicamente la etiqueta actual del botón de HDX que antes se llamaba RC. No es un modo, estado, comando ni valor de HARDATA ECP. Para que HDX informe remoteControlArmed = true, el operador debe presionar ese botón y mantener activo AUTO; ambas operaciones se realizan localmente en 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
  }
}

Estados de reproducción esperados: stopped, playing, paused. Tolerar valores futuros.

10.4 actualNextChanged

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

Ambos identificadores pueden ser 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
  }
}

Los cuatro identificadores pueden ser strings o null. positionMs, durationMs y remainingMs son la referencia temporal canónica. Los timers formateados son para presentación. phase puede cambiar con la evolución del reproductor; no usar una enumeración cerrada.

10.6 snapshot.window

Respuesta a 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
  }
}

El snapshot no incrementa playlistRevision.

10.7 snapshot.playlist y playlist.updated

Los nombres snapshot.playlist y playlist.updated aparecen directamente en 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 informa la lista actual sin incrementar la revisión.
  • playlist.updated representa una publicación posterior a mutación estructural e incrementa la revisión.
  • actualIndex, nextIndex, parentIndex, relativeStartOffsetSeconds, playlistItemId y materialId pueden ser null.
  • playlistItemId identifica una instancia concreta dentro de la playlist; se utiliza para seleccionar, mover u operar sobre ese nodo mediante HARDATA ECP.
  • materialId identifica el material en la plataforma HARDATA; se utiliza para consultar su metadata y recursos relacionados mediante HARDATA API.
  • playlistItemId y materialId no son intercambiables: un mismo material puede aparecer varias veces en una playlist y cada aparición tendrá su propio playlistItemId.
  • Tratar ambos identificadores como strings opacos, aunque su contenido parezca numérico. No deducir significado ni construirlos desde otros campos.
  • materialId puede ser null en bloques, agrupadores u otros nodos que no representen un material consultable mediante HARDATA API.
  • index es solo la posición actual del nodo y no debe utilizarse como identidad persistente.
  • Una recarga completa puede reasignar identificadores.
  • Reemplazar localmente la playlist completa por data.nodes al recibir cualquiera de los dos tipos.
  • Actualizar de forma atómica playlistRevision, punteros e items.

11. session.setup

HDX lo emite al completar el handshake interno. El Gateway guarda el último y lo entrega a cada sesión después de session.hello, incluso antes de autenticar:

{
  "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"
    ]
  }
}

La App debe tratar los campos como informativos y tolerar campos ausentes o capacidades nuevas. data.protocolVersion y los campos schemaVersion de otros mensajes describen datos internos transportados por el Gateway; no son otra versión del protocolo público ni deben usarse para despachar mensajes. Para ello se valida exclusivamente ecp.version. La recepción de session.setup no significa que la sesión esté autenticada.

12. Distribución, caché y correlación

El Gateway aplica estas reglas:

  • session.setup: para sesiones que enviaron session.hello.
  • Resultados y errores con requestId: solo para la sesión que originó el request.
  • Estados, snapshots y playlists públicos: solo para sesiones autenticadas.
  • playerStatus: además requiere suscripción para la ventana.

Antes de reenviar una acción a HDX, el Gateway sustituye el requestId por uno interno y agrega auditoría. Al volver la respuesta restaura el requestId original. Este mecanismo es transparente para la App.

El Gateway cachea el último mensaje público por connectionEpoch, ventana y evento/tipo. Al autenticar una App reproduce el cache ordenado por sequence. Por eso pueden llegar estados inmediatamente después del éxito de autenticación, antes de las respuestas a los snapshots solicitados por la App.

La App debe diseñar su store como un reductor idempotente, no asumir una secuencia rígida de eventos después de autenticar.

13. Errores generados por el Gateway

Todos los errores públicos usan ecp.type = "error":

{
  "ecp": {
    "version": "1.0",
    "type": "error",
    "requestId": "req-001"
  },
  "data": {
    "code": "controlRequired",
    "message": "Session does not own control"
  }
}
Código Significado Recuperación
invalidJson JSON inválido. Corregir serialización.
invalidEnvelope Falta el objeto ecp. Corregir envelope.
unsupportedVersion ecp.version no está soportada. Usar una versión publicada y aceptada por el Gateway.
helloRequired Se operó antes de session.hello. Enviar session.hello.
invalidParams Faltan parámetros públicos o tienen un tipo incorrecto. Corregir data.params.
authenticationRequired La sesión no está autenticada. Ejecutar authenticate.
controlRequired Acción mutante sin control. Pedir control y esperar control.granted.
authorizationPending HDX todavía no decidió. Esperar session.authorizationChanged.
applicationRejected HDX rechazó la identidad. El operador debe cambiar la decisión en HDX.
remoteControlNotArmed HDX no está armado para control externo. En HDX, presionar EXT CTRL (antes RC) y activar AUTO; luego esperar remoteControlArmed = true.
actionNotAvailable La acción pertenece a operación local de HDX y no forma parte de ECP. No enviarla desde una App externa.
hdxUnavailable Pipe desconectado o fallo de envío. Esperar reconexión y reautenticar.
requestTimeout HDX no terminó en el timeout configurado. Marcar resultado desconocido y resincronizar; no repetir acciones no idempotentes automáticamente.
unknownMessageType ecp.type desconocido. Corregir tipo.

Errores HDX observables adicionales:

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

La lista puede crecer. Mostrar data.message al operador y conservar data.code para lógica programática.

14. Máquina de estados recomendada

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

No habilitar controles mutantes en UI salvo en CONTROLLING. Las consultas y suscripciones se habilitan en OBSERVING y CONTROLLING.

15. Estado local recomendado

Mantener como mínimo:

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

Al cambiar connectionEpoch, reiniciar lastSequence, pendientes, autenticación, control y estados cuya vigencia dependa de la conexión HDX anterior.

16. Algoritmo mínimo de implementación

Pseudocódigo:

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. Arranque recomendado después de autenticar

Para cada ventana que la App mostrará:

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

Para una App controladora:

  1. indicar al operador que, en HDX, presione EXT CTRL y active AUTO si aún no están activos;
  2. esperar remoteControlArmed = true;
  3. enviar control.request;
  4. esperar control.granted;
  5. comprobar que HDX informó autorización approved;
  6. habilitar controles mutantes.

No es necesario esperar secuencialmente los tres primeros resultados para procesar eventos. Correlacionar cada request de forma independiente.

18. Reconexión segura

Ante cierre WebSocket:

  • marcar control y autenticación como perdidos;
  • marcar requests pendientes como resultado desconocido;
  • reconectar con backoff, por ejemplo 1, 2, 5, 10 segundos con jitter;
  • repetir desde session.hello.

Ante requestTimeout, no repetir automáticamente acciones mutantes como insertMaterial, deleteSelected, moveItemBefore o cambios de reproducción. La acción pudo haberse aplicado aunque su respuesta se haya perdido. Primero pedir snapshot.window y snapshot.playlist para reconciliar.

Las acciones idempotentes (setTalkOver, setStopAtEnd y valores absolutos de volumen/posición) pueden reintentarse solo después de resincronizar y recuperar el control.

19. Seguridad

  • No guardar ni registrar contraseñas en texto plano.
  • Enviar credenciales solo sobre loopback o WSS confiable.
  • No exponer el token interno Gateway-HDX; una App nunca debe usar gatewayHello.
  • Validar el certificado al usar WSS.
  • Limitar tamaño antes de serializar/enviar.
  • Escapar contenido visible como títulos; no interpretar HTML recibido.
  • Tratar nombres, títulos y mensajes de error como datos no confiables.
  • Separar permisos de observación de permisos de control en la UI.

20. Checklist de aceptación de una App

Transporte

  • Conecta por ws/wss y usa texto UTF-8.
  • Reensambla fragmentos y aplica límite de 1 MiB.
  • Serializa envíos concurrentes.

Sesión

  • Envía session.hello inmediatamente.
  • Procesa todos los tipos HARDATA ECP 1.0 conocidos.
  • Autentica cada nueva conexión/epoch.
  • No confunde session.setup con autenticación exitosa.

Control

  • Espera control.granted antes de mutar.
  • Espera autorización approved antes de solicitar control.
  • Deshabilita operación ante revocación, desarme o desconexión.
  • Libera control al cerrar ordenadamente.

HARDATA ECP

  • Usa ecp.version = "1.0" y params tipados.
  • Genera requestId único.
  • Correlaciona command.result, error y errores Gateway.
  • Mantiene accepted pendiente hasta resultado terminal.
  • Usa playlistItemId como identidad y no index.
  • Usa materialId —cuando está presente— para consultar el material mediante HARDATA API.
  • No confunde materialId con playlistItemId.
  • Descarta secuencias antiguas por ventana.
  • Reemplaza playlist atómicamente.
  • Tolera nulos, campos ausentes y extensiones.

Recuperación

  • Reinicia estado al cambiar connectionEpoch.
  • Reconstruye snapshots y suscripciones tras reconectar.
  • No repite mutaciones no idempotentes después de timeout sin reconciliar.

21. Flujo completo de ejemplo

21.1 Identificación

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

21.2 Autenticación

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

Esperar:

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

21.3 Sincronización

{"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":{}}

Solo después de control.granted:

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

21.5 Transferencia

Otra App aprobada puede enviar control.request. El controlador anterior recibe control.revoked con motivo replaced; no existe heartbeat de aplicación.


Con este contrato una App puede conectarse, autenticarse, observar estado, mantener playlists, adquirir control exclusivo, ejecutar acciones y recuperarse correctamente de desconexiones sin conocimiento del código interno del Gateway o de HDX Radio.