mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 10:58:28 +00:00
feat: add Mobile Control documentation and update table of contents
This commit is contained in:
parent
5a183e7333
commit
61b5ca6297
3 changed files with 349 additions and 9 deletions
|
|
@ -12,12 +12,18 @@ When routes are to be executed, Essentials will use this connection graph to dec
|
||||||
|
|
||||||
### Classes Referenced
|
### Classes Referenced
|
||||||
|
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingSource`
|
* `PepperDash.Essentials.Core.IRoutingSource`
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingOutputs`
|
* `PepperDash.Essentials.Core.IRoutingOutputs`
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingInputs`
|
* `PepperDash.Essentials.Core.IRoutingInputs`
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingInputsOutputs`
|
* `PepperDash.Essentials.Core.IRoutingMidpoint`
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingSinkNoSwitching`
|
* `PepperDash.Essentials.Core.IRoutingMidpointWithFeedback`
|
||||||
* `PepperDash.Essentials.Core.Routing.IRoutingSinkWithSwitching`
|
* `PepperDash.Essentials.Core.IRoutingSinkWithFeedback`
|
||||||
|
|
||||||
|
> **Note:** As of the v3 routing refactor, the older `IRoutingInputsOutputs`, `IRouting`,
|
||||||
|
> `IRoutingWithFeedback`, `IRoutingWithClear`, `IRoutingSink`, `IRoutingSinkWithInputPort`,
|
||||||
|
> `IRoutingSinkNoSwitching`, `IRoutingSinkWithSwitching`, and `IRoutingSinkWithSwitchingWithInputPort`
|
||||||
|
> interfaces have been removed and consolidated - see [Routing interfaces (v3)](#routing-interfaces-v3)
|
||||||
|
> below for the current mapping.
|
||||||
|
|
||||||
## Example system, a simple presentation system
|
## Example system, a simple presentation system
|
||||||
|
|
||||||
|
|
@ -56,11 +62,12 @@ Source devices in Essentials must implement `IRoutingOutputs` or `IRoutingSource
|
||||||
|
|
||||||
A midpoint is a device in the middle of the signal chain. Typically a switcher, matrix or otherwise. Examples: DM chassis; DM-TX; DM-RMC; A video codec. These devices will have input and output ports.
|
A midpoint is a device in the middle of the signal chain. Typically a switcher, matrix or otherwise. Examples: DM chassis; DM-TX; DM-RMC; A video codec. These devices will have input and output ports.
|
||||||
|
|
||||||
Midpoint devices must implement `IRoutingInputsOutputs`. Midpoints with switching must implement `IRouting`.
|
Passthrough midpoints (no active switching) must implement `IRoutingMidpoint`. Midpoints that actively
|
||||||
|
switch and report their current routes must implement `IRoutingMidpointWithFeedback`.
|
||||||
|
|
||||||
#### Sink
|
#### Sink
|
||||||
|
|
||||||
A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkNoSwitching` or `IRoutingSinkWithSwitching`.
|
A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkWithFeedback`.
|
||||||
|
|
||||||
#### Tie-line
|
#### Tie-line
|
||||||
|
|
||||||
|
|
@ -78,4 +85,95 @@ A tie-line is a logical representation of a physical cable connection between tw
|
||||||
|
|
||||||
### Interfaces
|
### Interfaces
|
||||||
|
|
||||||
Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs
|
#### Routing interfaces (v3)
|
||||||
|
|
||||||
|
The v3 routing refactor consolidated several older, overlapping interfaces into two feedback-aware
|
||||||
|
interfaces. If you're updating a device implemented against the pre-v3 API, use this table to find
|
||||||
|
its replacement:
|
||||||
|
|
||||||
|
| Old interface (pre-v3, removed) | Current interface |
|
||||||
|
| --- | --- |
|
||||||
|
| `IRoutingInputsOutputs` | `IRoutingMidpoint` |
|
||||||
|
| `IRouting`, `IRoutingWithFeedback`, `IRoutingWithClear` | `IRoutingMidpointWithFeedback` |
|
||||||
|
| `IRoutingSink`, `IRoutingSinkWithInputPort`, `IRoutingSinkWithSwitching`, `IRoutingSinkWithSwitchingWithInputPort` | `IRoutingSinkWithFeedback` |
|
||||||
|
|
||||||
|
**Base building-block interfaces** (unchanged):
|
||||||
|
|
||||||
|
* `IRoutingSource` - marker interface for a device that originates a signal path; extends `IRoutingOutputs`.
|
||||||
|
* `IRoutingOutputs` - exposes `RoutingPortCollection<RoutingOutputPort> OutputPorts`.
|
||||||
|
* `IRoutingInputs` - exposes `RoutingPortCollection<RoutingInputPort> InputPorts`.
|
||||||
|
|
||||||
|
**Midpoint interfaces:**
|
||||||
|
|
||||||
|
* `IRoutingMidpoint` - a passthrough device with both input and output ports but no active switching. Extends `IRoutingInputs` and `IRoutingOutputs`.
|
||||||
|
* `IRoutingMidpointWithFeedback` - a midpoint that actively switches and reports feedback. Extends `IRoutingMidpoint` and adds:
|
||||||
|
* `void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)`
|
||||||
|
* `void ClearRoute(object outputSelector, eRoutingSignalType signalType)`
|
||||||
|
* `List<RouteSwitchDescriptor> CurrentRoutes { get; }`
|
||||||
|
* `event RouteChangedEventHandler RouteChanged`
|
||||||
|
|
||||||
|
**Sink interface:**
|
||||||
|
|
||||||
|
* `IRoutingSinkWithFeedback` - a routing endpoint device that can switch inputs and provides feedback. Extends `IRoutingInputs`, `IKeyName`, and `ICurrentSources` (see below), and adds:
|
||||||
|
* `void ExecuteSwitch(object inputSelector)`
|
||||||
|
* `RoutingInputPort CurrentInputPort { get; }`
|
||||||
|
* `event InputChangedEventHandler InputChanged`
|
||||||
|
|
||||||
|
#### Current source tracking (`ICurrentSources`)
|
||||||
|
|
||||||
|
`ICurrentSources` is a new interface (implemented by `IRoutingSinkWithFeedback`) that reports which
|
||||||
|
source is actually feeding a sink right now, per signal type:
|
||||||
|
|
||||||
|
* `Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; }`
|
||||||
|
* `Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; }`
|
||||||
|
* `event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged`
|
||||||
|
|
||||||
|
A new `RoutingFeedbackManager` subscribes to `RouteChanged` on every `IRoutingMidpointWithFeedback`
|
||||||
|
and `InputChanged` on every `IRoutingSinkWithFeedback` in the system, traces each change back through
|
||||||
|
the tie-line graph to the originating source (debounced ~500ms), and keeps each sink's
|
||||||
|
`ICurrentSources` state up to date automatically - developers no longer need to manually wire up
|
||||||
|
source tracking per device.
|
||||||
|
|
||||||
|
#### Multiview layout support
|
||||||
|
|
||||||
|
For decoders that can display several independently-routable tiles at once (e.g. a WyreStorm
|
||||||
|
NetworkHD-style multiview decoder), two additional interfaces are available:
|
||||||
|
|
||||||
|
* `IRoutingSinkWithLayouts` - extends `IRoutingSource` and exposes `Dictionary<int, IRoutingSinkWithFeedback> WindowTileSinks`, one per-tile "virtual sink" that can be routed independently like any other `IRoutingSinkWithFeedback`.
|
||||||
|
* `IRoutingSinkWithLayoutState` - extends `IRoutingSinkWithLayouts` and additionally reports the current canvas/tile geometry via `MultiviewLayoutState CurrentLayout { get; }` and `event EventHandler<MultiviewLayoutStateEventArgs> LayoutChanged`.
|
||||||
|
|
||||||
|
`MultiviewLayoutState` is a JSON-serializable, product-agnostic model of the canvas: `CanvasWidth`,
|
||||||
|
`CanvasHeight`, and a list of `MultiviewTileState` entries (`TileNumber`, `TileSinkKey`, `X`, `Y`,
|
||||||
|
`Width`, `Height`, `ZOrder`, `SourceDeviceKey`) - suitable for driving a routing diagram UI.
|
||||||
|
|
||||||
|
#### Real-time routing feedback over WebSocket
|
||||||
|
|
||||||
|
Essentials Core includes a built-in WebSocket service, `RoutingFeedbackWebsocket`
|
||||||
|
(`PepperDash.Essentials.Core.Web`), that broadcasts routing state changes to any connected client in
|
||||||
|
real time - this is what powers the live routing diagram in the
|
||||||
|
[Essentials Web Config App](https://github.com/PepperDash/essentials-web-config-app) and
|
||||||
|
[essentials-devtools](https://github.com/PepperDash/essentials-devtools).
|
||||||
|
|
||||||
|
A client starts (or reuses) a session by sending an HTTP `GET` to the `routingFeedbackSession` API
|
||||||
|
route (e.g. `https://[processor-ip]/cws/[appId]/api/routingFeedbackSession`), which starts the
|
||||||
|
WebSocket server on a random high port (if not already running), forwards that port on the CS LAN
|
||||||
|
adapter if present, and returns a JSON body with the connection URL(s):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "url": "wss://[processor-ip]:[port]/routing/join/", "fallbackUrl": "wss://..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
Once connected, the client receives a `snapshot` message describing every midpoint route, sink
|
||||||
|
input, and multiview layout currently active, followed by incremental update messages (each
|
||||||
|
debounced ~200ms) as things change:
|
||||||
|
|
||||||
|
| Message `type` | Sent when | Payload |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `snapshot` | On connect | `midpointRoutes`, `sinkRoutes`, `layouts` for the whole system |
|
||||||
|
| `midpointRouteChanged` | An `IRoutingMidpointWithFeedback`'s `RouteChanged` fires | `deviceKey`, `routes: [{ inputPortKey, outputPortKey, signalType }]` |
|
||||||
|
| `sinkInputChanged` | An `IRoutingSinkWithFeedback`'s `InputChanged` fires | `deviceKey`, `inputPortKey`, `sourceDeviceKey`, `signalType` |
|
||||||
|
| `layoutChanged` | An `IRoutingSinkWithLayoutState`'s `LayoutChanged` fires | `deviceKey`, `layout` (a `MultiviewLayoutState`) |
|
||||||
|
|
||||||
|
Per-tile sinks belonging to an `IRoutingSinkWithLayouts` device are reported to clients as qualified
|
||||||
|
input ports on their parent device's node, rather than as separate graph nodes, so a routing diagram
|
||||||
|
can render a multiview decoder as a single device with per-tile inputs.
|
||||||
|
|
|
||||||
240
docs/docs/technical-docs/Mobile-Control.md
Normal file
240
docs/docs/technical-docs/Mobile-Control.md
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
# Mobile Control: architecture and client communication
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Mobile Control is a WebSocket-based messaging layer built into Essentials. Devices expose small
|
||||||
|
"messenger" classes that register JSON message paths (e.g. `/device/{key}/startMeeting`) with a
|
||||||
|
central `MobileControlSystemController`. Clients (typically a React app built on
|
||||||
|
[`@pepperdash/mobile-control-react-app-core`](https://github.com/PepperDash/mobile-control-react-app-core))
|
||||||
|
connect over WebSocket with a per-room token, send the same JSON envelope to invoke device methods,
|
||||||
|
and receive unsolicited state-update messages that a Redux store uses to keep device/room state in
|
||||||
|
sync in real time.
|
||||||
|
|
||||||
|
## Server-side architecture
|
||||||
|
|
||||||
|
### Projects involved
|
||||||
|
|
||||||
|
* `PepperDash.Essentials.MobileControl` - the `MobileControlSystemController` device, its
|
||||||
|
WebSocket server (`WebSocketServer/MobileControlWebsocketServer.cs`), room bridges, and
|
||||||
|
touchpanel controller.
|
||||||
|
* `PepperDash.Essentials.MobileControl.Messengers` (namespace `PepperDash.Essentials.AppServer.Messengers`) -
|
||||||
|
the `MessengerBase` class and the library of built-in messengers for common device interfaces
|
||||||
|
(`IHasStartMeetingMessenger`, `IHasMeetingInfoMessenger`, `ICommunicationMonitorMessenger`,
|
||||||
|
`IRoutingMidpointWithFeedbackMessenger`, etc.), plus the message envelope types.
|
||||||
|
* `PepperDash.Essentials.Core.DeviceTypeInterfaces` - the public contracts:
|
||||||
|
`IMobileControl`, `IMobileControlMessenger`, `IMobileControlMessengerWithSubscriptions`,
|
||||||
|
`IMobileControlRoomMessenger`, `IMobileControlAction`, `IMobileControlMessage`, and the
|
||||||
|
touchpanel-controller interfaces.
|
||||||
|
|
||||||
|
### Direct Server vs. Edge Server
|
||||||
|
|
||||||
|
Mobile Control supports two (non-exclusive) transport modes, configured on the `MobileControlConfig`
|
||||||
|
device:
|
||||||
|
|
||||||
|
* **Direct Server** - `MobileControlWebsocketServer` runs an HTTPS WebSocket server directly on the
|
||||||
|
processor hardware. Clients on the same network connect straight to the processor - lowest
|
||||||
|
latency, no external dependency. This is what `mobile-control-react-app-core`'s local dev flow
|
||||||
|
and the deployed `mcUserApp` React app use.
|
||||||
|
* **Edge Server** (API server / cloud gateway) - the processor instead makes an *outbound* WebSocket
|
||||||
|
connection to an external Mobile Control server, which relays messages between it and remote
|
||||||
|
clients. Useful when clients can't reach the processor directly (e.g. no local network access).
|
||||||
|
|
||||||
|
Both modes can be enabled simultaneously; outgoing messages are queued and sent down whichever
|
||||||
|
transport(s) are active.
|
||||||
|
|
||||||
|
### The WebSocket endpoint (Direct Server)
|
||||||
|
|
||||||
|
`MobileControlWebsocketServer` (`WebSocketServer/MobileControlWebsocketServer.cs`) hosts the
|
||||||
|
Direct Server:
|
||||||
|
|
||||||
|
* **Path**: `/mc/api/ui/join/` (`_wsPath`)
|
||||||
|
* **Port**: `50000 + <program slot number>` by default (e.g. program slot 2 → port `50002`), or a
|
||||||
|
custom port from config
|
||||||
|
* **User app**: static files are served from `/user/programX/mcUserApp` (`_appPath`) at the base
|
||||||
|
href `/mc/app` (`_userAppBaseHref`) - this is exactly the `mcUserApp` deployment directory used
|
||||||
|
when [deploying](../Get-started.md) a built React app.
|
||||||
|
|
||||||
|
### Connecting and authentication (tokens)
|
||||||
|
|
||||||
|
A client doesn't connect straight to the WebSocket - it first needs a **token** identifying which
|
||||||
|
room/UI-client slot it's joining:
|
||||||
|
|
||||||
|
1. **Get a token** - on the processor console, run:
|
||||||
|
```
|
||||||
|
mobileinfo:[programSlot]
|
||||||
|
```
|
||||||
|
This prints the Direct Server port, any existing UI-client tokens, and full connect URLs (e.g.
|
||||||
|
`http://[ip]:[port]/mc/app?token=[token]`). New tokens can also be added dynamically
|
||||||
|
(`mobileadduiclient:[programSlot] [roomKey]`) - see the console command's help text for details.
|
||||||
|
2. **Join a room** - the client calls `GET {apiPath}/ui/joinroom?token={token}` over HTTPS, which
|
||||||
|
validates the token and returns room data including a generated `clientId`.
|
||||||
|
3. **Open the WebSocket** - the client connects to
|
||||||
|
`wss://[processor-ip]:[port]/mc/api/ui/join/{token}?clientId={clientId}`.
|
||||||
|
|
||||||
|
Each connected client is tracked as a `UiClient`; disconnecting removes its subscriptions from
|
||||||
|
every messenger it was subscribed to.
|
||||||
|
|
||||||
|
### The message envelope
|
||||||
|
|
||||||
|
Every message sent in either direction is the same simple JSON envelope
|
||||||
|
(`MobileControlMessage`, namespace `PepperDash.Essentials.AppServer.Messengers`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "/device/zoomRoom1/startMeeting",
|
||||||
|
"clientId": "abc-123",
|
||||||
|
"content": { "value": 30 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* `type` - a path identifying the target device/room and action (client → server), or the
|
||||||
|
device/room whose state is being reported (server → client).
|
||||||
|
* `clientId` - which client sent the message, or (server → client) which single client a targeted
|
||||||
|
reply is meant for; omitted/ignored for broadcasts to all subscribers.
|
||||||
|
* `content` - a `JToken` payload. For simple values, `MobileControlSimpleContent<T>` wraps a single
|
||||||
|
`value` property (as in the example above); for state updates, `content` is the device's full
|
||||||
|
serialized state object.
|
||||||
|
|
||||||
|
### The messenger pattern
|
||||||
|
|
||||||
|
Each device that wants to be controllable/observable over Mobile Control gets a small **messenger**
|
||||||
|
class deriving from `MessengerBase` (`PepperDash.Essentials.AppServer.Messengers`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class IHasStartMeetingMessenger : MessengerBase
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
protected override void RegisterActions()
|
||||||
|
{
|
||||||
|
AddAction("/fullStatus", (id, content) => SendFullStatus(id));
|
||||||
|
|
||||||
|
AddAction("/startMeeting", (id, content) =>
|
||||||
|
{
|
||||||
|
var msg = content.ToObject<MobileControlSimpleContent<uint>>();
|
||||||
|
_startMeeting.StartMeeting(msg?.Value ?? _startMeeting.DefaultMeetingDurationMin);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAction("/leaveMeeting", (id, content) => _startMeeting.LeaveMeeting());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* The messenger is constructed with a **base `MessagePath`** (e.g. `/device/zoomRoom1`) and calls
|
||||||
|
`AddAction(subPath, handler)` for each supported action, relative to that base path.
|
||||||
|
* `RegisterWithAppServer(IMobileControl appServerController)` registers the messenger's
|
||||||
|
`HandleMessage` callback with the system controller via `IMobileControl.AddAction<T>(...)`.
|
||||||
|
* When an incoming message's `type` starts with the messenger's `MessagePath`, `HandleMessage`
|
||||||
|
strips that prefix (leaving e.g. `/startMeeting`) and dispatches to the matching registered
|
||||||
|
action. The sending client is automatically added to that messenger's subscriber list, so it
|
||||||
|
receives future unsolicited feedback from it.
|
||||||
|
* To push state out, messengers call `PostStatusMessage(DeviceStateMessageBase message, clientId)` -
|
||||||
|
omitting `clientId` broadcasts to every subscribed client; passing one targets a single client
|
||||||
|
(e.g. replying to a `/fullStatus` request).
|
||||||
|
|
||||||
|
### How devices opt in
|
||||||
|
|
||||||
|
`EssentialsDevice.CustomActivate()` calls a virtual `CreateMobileControlMessengers()` hook once all
|
||||||
|
devices have activated. A device (or a room) overrides this method to look up the `IMobileControl`
|
||||||
|
device and construct/register whichever messengers it needs:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override void CreateMobileControlMessengers()
|
||||||
|
{
|
||||||
|
var mc = DeviceManager.AllDevices.OfType<IMobileControl>().FirstOrDefault();
|
||||||
|
if (mc == null) return;
|
||||||
|
|
||||||
|
var messenger = new IHasStartMeetingMessenger("zoomRoom1-startMeeting", "/device/zoomRoom1", this);
|
||||||
|
messenger.RegisterWithAppServer(mc);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rooms typically implement `IMobileControlRoomMessenger` and register a room-level messenger at a
|
||||||
|
`/room/{roomKey}` path (via a `MobileControlBridgeBase`/`MobileControlEssentialsRoomBridge`), in
|
||||||
|
addition to any per-device messengers.
|
||||||
|
|
||||||
|
## Client-side architecture (React)
|
||||||
|
|
||||||
|
`@pepperdash/mobile-control-react-app-core` provides the client building blocks used by apps like
|
||||||
|
[beincourt-pv2-react-app](https://github.com/pepperdash-beincourt/beincourt-pv2-react-app):
|
||||||
|
|
||||||
|
### Connection lifecycle
|
||||||
|
|
||||||
|
The real work happens in a Redux middleware (`src/lib/store/middleware/websocketMiddleware.ts`);
|
||||||
|
`WebsocketProvider`/`WebsocketContext` are a thin, backward-compatible React Context wrapper around
|
||||||
|
it that simply dispatches Redux actions (`wsConnect`, `wsSendMessage`, `wsReconnect`, ...).
|
||||||
|
|
||||||
|
1. On mount, `wsConnect()` is dispatched.
|
||||||
|
2. The middleware reads `apiPath` from the app's local config (`_config.local.json`/
|
||||||
|
`_config.default.json`) and the connection `token` (from the URL's `?token=` query param).
|
||||||
|
3. It calls `GET {apiPath}/ui/joinroom?token={token}` to validate the token and get room data
|
||||||
|
(including a `clientId`).
|
||||||
|
4. It opens a WebSocket at `{apiPath with ws(s) scheme}/ui/join/{token}?clientId={clientId}`.
|
||||||
|
5. On specific close codes (e.g. `4000` user code changed, `4002` room combination changed) it stops
|
||||||
|
auto-reconnecting and surfaces an error; otherwise it automatically retries.
|
||||||
|
|
||||||
|
### Sending messages
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { sendMessage } = useWebsocketContext();
|
||||||
|
sendMessage('/device/zoomRoom1/startMeeting', { value: 30 });
|
||||||
|
```
|
||||||
|
|
||||||
|
`sendMessage` serializes `{ type, clientId, content }` (the same envelope described above) and
|
||||||
|
sends it over the open WebSocket.
|
||||||
|
|
||||||
|
### Receiving messages / state hydration
|
||||||
|
|
||||||
|
Incoming messages are routed by their `type` prefix:
|
||||||
|
|
||||||
|
| Prefix | Handling |
|
||||||
|
| --- | --- |
|
||||||
|
| `/system/*` | Internal system messages (user code, touchpanel key, room-combination/device-interface changes, initial sync complete, ...) |
|
||||||
|
| `/event/*` | Dispatched to any handlers registered via `addEventHandler(eventType, key, callback)` |
|
||||||
|
| `/room/*` | `dispatch(roomsActions.setRoomState(message))` |
|
||||||
|
| `/device/*` | `dispatch(devicesActions.setDeviceState(message))` |
|
||||||
|
|
||||||
|
Device/room state lives in Redux slices keyed by device/room key, so any component can read the
|
||||||
|
latest known state for a given key.
|
||||||
|
|
||||||
|
### The hook pattern
|
||||||
|
|
||||||
|
Each supported device interface has a small hook that combines reading state with calling actions,
|
||||||
|
e.g. `useIHasStartMeeting`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useIHasStartMeeting(key: string): IHasStartMeetingReturn | undefined {
|
||||||
|
const { sendMessage } = useWebsocketContext();
|
||||||
|
const state = useGetDevice<IHasStartMeetingState>(key); // reads devices[key] from Redux
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!state) return undefined;
|
||||||
|
const path = `/device/${key}`;
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
startMeeting: (durationMin?: number) => sendMessage(`${path}/startMeeting`, { value: durationMin }),
|
||||||
|
leaveMeeting: () => sendMessage(`${path}/leaveMeeting`, null),
|
||||||
|
};
|
||||||
|
}, [key, sendMessage, state]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Components use these hooks to read live device state and call device actions without needing to
|
||||||
|
know anything about the WebSocket transport underneath.
|
||||||
|
|
||||||
|
## Full round-trip example: starting a meeting
|
||||||
|
|
||||||
|
1. **UI**: user clicks "Start Meeting"; component calls `startMeeting(30)` from `useIHasStartMeeting('zoomRoom1')`.
|
||||||
|
2. **Client → server**: `sendMessage('/device/zoomRoom1/startMeeting', { value: 30 })` sends
|
||||||
|
`{"type":"/device/zoomRoom1/startMeeting","clientId":"abc-123","content":{"value":30}}` over the WebSocket.
|
||||||
|
3. **Server routing**: the system controller matches `/device/zoomRoom1` against the registered
|
||||||
|
`IHasStartMeetingMessenger`'s `MessagePath`, strips the prefix to get `/startMeeting`, and invokes
|
||||||
|
its registered action, which calls `StartMeeting(30)` on the underlying device.
|
||||||
|
4. **Device fires feedback**: the device's meeting-started feedback fires.
|
||||||
|
5. **Server → clients**: the messenger calls `PostStatusMessage(...)`, broadcasting
|
||||||
|
`{"type":"/device/zoomRoom1","clientId":null,"content":{ ...state }}` to every client subscribed
|
||||||
|
to that messenger.
|
||||||
|
6. **Client receives**: the message's `type` starts with `/device/`, so
|
||||||
|
`dispatch(devicesActions.setDeviceState(message))` merges the new state into Redux under
|
||||||
|
`devices['zoomRoom1']`.
|
||||||
|
7. **UI updates**: `useIHasStartMeeting('zoomRoom1')`'s `state` reflects the new meeting info on the
|
||||||
|
next render.
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
href: technical-docs/Plugins.md
|
href: technical-docs/Plugins.md
|
||||||
- name: Communication Basics
|
- name: Communication Basics
|
||||||
href: technical-docs/Communication-Basics.md
|
href: technical-docs/Communication-Basics.md
|
||||||
|
- name: Mobile Control
|
||||||
|
href: technical-docs/Mobile-Control.md
|
||||||
- name: Debugging
|
- name: Debugging
|
||||||
href: technical-docs/Debugging.md
|
href: technical-docs/Debugging.md
|
||||||
- name: Feedback Classes
|
- name: Feedback Classes
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue