The watch talks to the Blazor Companion PWA (and, ultimately, the AI Assistant backend) over one
authenticated WebRTC data channel, shared by the OS and all loadable apps through a multiplexed
channel bus. This is the foundation the flagship AI Assistant is built on. Status: live and proven
end to end as of 2026-06-23 (Phase 7). The sys.files lane (chunked SD read/write over this link)
was hardware-verified 2026-06-24. The watch-as-answerer DTLS-role fix shipped the same day in
SpawnDev.SIPSorcery 10.0.7 + SpawnDev.RTC 1.1.11.
apps ──IAppChannel──┐ ┌── app.<id>.* surface (Companion / PWA)
OS ──Bus.Send─────┤ TransportBus │
│ (named channels, │
│ namespaced, │
│ isolated) │
└──────────┬───────────┘
│ WebRtcDataFraming [cidLen][cid][plen LE][payload]
one WebRTC data channel (DTLS-SRTP encrypted)
│
authenticated at open (mutual Ed25519, BLE-paired identities)
- Watch side:
WebRtcTransportService(autonomous: connect → stay → reconnect, gated paired+WiFi) owns the connection on its own thread and pumpsTransportBus. The native WebRTC is libpeer (SpawnDev.WebRTCinterop, lock-free TX/RX rings so the CLR never stalls). - Companion side:
SpawnWear.Bridge(WebRtcTransportover SpawnDev.RTC) +BridgeClient.
One link carries many logical channels, keyed by the channel id in each frame. Both the OS and apps send/receive through the bus; they cannot collide, spoof, or crash each other:
- Namespacing. The OS owns reserved names (
battery,imu,rtc,sys.*, ...). Apps get a scopedIAppChannel(Bus.OpenAppChannel(appId)) that forces every send/subscribe intoapp.<appId>.*- an app physically cannot touch a system channel or another app's channels. - Isolation. Inbound frames route to the registered handler with per-handler exception catching;
a throwing app handler can't take down the pump or the others. App channels auto-unsubscribe on
Close()(call it when the app unloads). - Send queue.
Bus.Sendenqueues (thread-safe, bounded drop-oldest); the pump loop drains to the wire. Callers never touch the radio. The actual nativeSendis lock-free (TX ring).
The bus carries opaque bytes per channel, so each lane picks its own encoding:
- System lanes: compact fixed binary schemas (the same ones
WatchProfileServicenotifies over BLE - e.g.battery=[percent:u8][flags:u8][mV:u16-LE][mA:i16-LE]). The Companion'sBridgeClientalready decodes these. - App lanes: MessagePack - typed, self-describing, evolvable. The watch encodes with
Services/MsgPackWriter.cs(a minimal hand-rolled nanoFramework encoder: maps/arrays/strings/ints/ float32/bool/nil, big-endian; the full MessagePack-CSharp can't run on nanoFramework). The Companion (.NET) and PWA (JS wrapper) decode with the full MessagePack libraries.
- Graceful: the Companion sends
sys.disconnectright before teardown → the watch flips its link state immediately. - Ungraceful (crash, dead WiFi, browser killed): libpeer's ICE keepalive timeout
(
CONFIG_KEEPALIVE_TIMEOUT, 10s) - no peer STUN binding-requests for that long → link closed.
var ch = transportBus.OpenAppChannel("myapp"); // confined to app.myapp.*
ch.OnMessage("cmd", (cid, payload) => { /* decode MessagePack, act */ });
var w = new MsgPackWriter(); w.WriteMapHeader(2);
w.WriteString("type"); w.WriteString("hello");
w.WriteString("v"); w.WriteInt(1);
ch.Send("event", w.ToArray()); // -> app.myapp.event
// on unload:
ch.Close();The Companion-side surface subscribes to app.myapp.* (via Bridge.GetUnderlyingTransport() .MessageReceived, filtered by channel id) and decodes the MessagePack. The AI Assistant follows
exactly this: an app.assistant.* lane carrying voice/text/command messages, multiplexed with the OS
telemetry over the same authenticated link.
| Piece | File |
|---|---|
| Autonomous connection service | SpawnWear/Services/WebRtcTransportService.cs |
| Channel bus + IAppChannel | SpawnWear/Services/TransportBus.cs |
| MessagePack encoder (watch) | SpawnWear/Services/MsgPackWriter.cs |
| Connect/challenge/pump loop | SpawnWear/Program.cs (WebRtcConnectRun) |
| Native WebRTC interop (lock-free) | nf-interpreter InteropAssemblies/SpawnDev.WebRTC/...PeerConnection.cpp |
| libpeer (forked, open) | LostBeard/libpeer branch spawnwear |
| Companion transport / decode | SpawnWear.Bridge/WebRtc/WebRtcTransport.cs, BridgeClient.cs; SpawnWear.Companion/Pages/Home.razor |