@imperosoft/cris-webui-ch5-core
v1.6.2
Published
CRIS WebUI CH5 Core - Crestron CH5/CrComLib and custom-WebSocket integration for React
Maintainers
Readme
@imperosoft/cris-webui-ch5-core
CRIS WebUI Core — Crestron integration for React (Zustand stores + hooks) with two selectable transports:
ch5— Crestron native stack: CrComLib on-panel, WebXPanel in-browser. For panels that run CH5 user projects (TS-x70, TS-1080, …).ws— custom WebSocket protocol against the C# module on the processor. For panels that only load a plain web page (TSS-1080 scheduling panels, kiosks, any browser).
Same app, same hooks, same stores on both transports. Scope on ws: global joins + custom objects (smart objects are ch5-only — their single production use, the custom-object pipe on SO 10001, is native JSON over the websocket).
Initialization
import { initBridge } from '@imperosoft/cris-webui-ch5-core';
await initBridge({
transport: 'auto', // 'ch5' | 'ws' | 'auto' (default 'auto')
debug: true,
});Transport resolution ('auto')
- Explicit
transport: 'ch5' | 'ws'in the config — always wins. - URL query param
?transport=ws|ch5— how a web-only panel opts in without an app rebuild: point it athttp://processor:port/?transport=ws. - Vite define
__CRIS_TRANSPORT__(dev only — applied when the page runs on localhost). - Fallback
ch5— a plain browser cannot be distinguished from a web-only panel, so auto preserves the pre-1.5 behavior and every existing deployment upgrades unchanged.
Config fields
| Field | Transport | Meaning |
|---|---|---|
| transport | — | 'ch5' \| 'ws' \| 'auto' (default 'auto') |
| processorHost | both | Processor host (fallbacks: ?host= → __PROCESSOR_HOST__ → page hostname) |
| ipId | both | IPID (ch5: CIP connection; ws: ?ipid= query param on the socket URL) |
| debug | both | Console logging |
| onConnectionChange | both | (connected: boolean) => void |
| roomId, authToken, forceHttps | ch5 only | VC-4 / WebXPanel auth / https redirect guard |
| appNumber | ws only | Dev-mode port rule: 10000 + appNumber (default 1; also ?app=, __PROCESSOR_APP_NUMBER__) |
| wsPort | ws only | Explicit port override (also ?wsPort=) |
| reconnectIntervalMs | ws only | Reconnect interval (default 5000) |
ws endpoint rules
- Production (page served by the processor):
ws://<page host>:<page port>— same origin. - Dev (page on localhost):
ws://<processorHost>:<10000 + appNumber>. - Scheme follows the page: an
https:page connects withwss:(the C# server must then offer TLS); plain-http pages usews:. 'connected'status is granted only after the backend answers the__CFG__handshake — identical semantics to the ch5 transport.
Local testing without a processor:
npm run mock:ws # protocol mock on ws://localhost:10001
npm run test:ws # headless end-to-end test against the mockThen open the consuming app with http://localhost:5173/?transport=ws&host=localhost&wsPort=10001.
Wire protocol (ws transport) — contract for the C# module
One WebSocket per panel. Text frames, UTF-8, one complete JSON object per frame. Envelope:
{ "header": "<channel>", "action": "<verb>", "parameters": { } }Unknown headers/actions MUST be ignored by both sides (forward compatibility). All join values are strings on the wire.
joins channel (byte-compatible with the legacy cris-webui-core protocol)
Server → client, full snapshot. MUST be sent unsolicited immediately after every socket accept — this is the reconnect resync mechanism. Empty arrays allowed.
{ "header": "joins", "action": "status", "parameters": { "joins": {
"digitals": [ { "number": 1, "value": "True" } ],
"analogs": [ { "number": 3, "value": "42" } ],
"serials": [ { "number": 2, "value": "hello" } ] } } }Server → client, delta:
{ "header": "joins", "action": "changed", "parameters": {
"join": { "type": "digital", "number": 1 }, "value": "False" } }Client → server, write:
{ "header": "joins", "action": "change", "parameters": {
"join": { "type": "analog", "number": 3 }, "value": "500" } }type ∈ digital | analog | serial. Digital: exactly "True" / "False" (the client tolerates lowercase inbound). Analog: decimal integer string 0–65535. The client never updates its local state on write — it waits for the server's changed feedback (state truth lives on the processor).
customs channel (new)
parameters is exactly the {oid, data} JSON that rides the SO-10001/join-1 serial pipe on the ch5 transport — the C# custom-object layer can route both transports into one handler. action is always "message"; the verb lives inside data, bit-identical with ch5.
Server → client:
{ "header": "customs", "action": "message", "parameters": {
"oid": "DSP1_API", "data": { "status": { "...": "..." } } } }data.status = full replace; data.update = partial deep-merge (arrays merged by id).
Client → server (same envelope): free-form app payloads, plus the control verbs:
{ "oid": "__CFG__", "data": { "action": "init" } }
{ "oid": "DSP1_API", "data": { "action": "subscribe" } }
{ "oid": "DSP1_API", "data": { "action": "unsubscribe" } }__CFG__ contract. The server MUST answer {action:"init"} with:
{ "header": "customs", "action": "message", "parameters": {
"oid": "__CFG__", "data": { "status": {
"re": { "wi": 1920, "he": 1080 }, "dl": "layoutA", "cc": { } } } } }and MUST also push it unsolicited when the server itself (re)starts. The client shows 'connected' only after this status arrives; a backend that accepts sockets but never answers leaves panels in 'connecting' — this is deliberate. A new connection is a blank slate: the server drops prior opt-in subscriptions; the client re-sends subscribe for every active oid after each __CFG__ status.
Connection sequence
Client Server (C# module)
|-- WS CONNECT ws://host:port/?ipid=21 --->|
|<-- accept --------------------------------|
|<-- joins/status (full snapshot) ----------| unsolicited, REQUIRED
|-- customs __CFG__ {action:init} --------->| retried 2s x10, then 10s
|<-- customs __CFG__ {status:{re,dl,cc}} ---| -> client status = 'connected'
|-- customs {action:subscribe} x N -------->| replay of active opt-in oids
|<-- customs <oid> {status:...} ------------| snapshot per subscribed oid
|<== steady state: joins changed/change + customs both ways ==>
... on drop: client reconnects every 5s; sequence repeats from accept.Heartbeat: none in v1. The header "sys" ({action:"ping"|"pong"}) is reserved for a future opt-in keepalive; the ignore-unknown rule makes adding it non-breaking.
Development
npm run build # tsc + vite library build (ES + CJS + bundled d.ts)
npm run dev # vite build --watch
npm run mock:ws # dev mock of the C# ws module
npm run test:ws # headless ws end-to-end test (build first)Architecture notes
src/bridge.ts— ch5 transport (CrComLib/WebXPanel, https guard, online signal,__CFG__loop).src/ws-bridge.ts— ws transport (this protocol).src/transport.ts— router: transport resolution + the publicinitBridge/cleanupBridge/isConnected/isInitialized/subscribeJoin/subscribeSmartJoin.src/transport-shared.ts— transport-neutral state (active kind, logging, connected-notifier registry).- Writes are decoupled from transports via the
windowCustomEvent bus'ch5:publish'; reads are fed into the Zustand stores by whichever bridge is active. src/custom-object-store.ts— transport-blind message processor (deep-merge, ref-counted opt-in subscriptions,__CFG__→ GUI store push).
