# spt-lecturn

A Playdate-on-a-lecturn as a one-handed dial Gateway into an SPT subnet: crank-driven smooth scroll, macros, and media control against any subnet machine, plus a pocket window onto agents (digest view, spoken replies). Adapters: `spt-lecturn` (the Gateway endpoint), `spt-usbip-driver` (a generic USB/IP Shell, standalone product value), and `spt-lecturn-companion` (optional target-side sensing Shell, v1.x). Sibling of `spt-core` (the Gateway/Shell/tunnel/profile model lives there, in its CONTEXT.md); heir to the `playdial` USB-serial experiment.

## Language

### The device and its presence

**lecturn**:
The physical unit — the Playdate seated in the lecturn stand, operated one-handed (crank + buttons + onboard mic). The user-facing name for the whole input surface.
_Avoid_: "the playdate" when meaning the product (the Playdate is the hardware inside it); "dial".

**spt-lecturn (Gateway endpoint)**:
The lecturn's presence in the subnet — a **Gateway** endpoint (spt-core type: human-backed, owns Shells, no LLM; its sends are `user-msg`-typed, carrying the user's authority). The decision-making half lives in the Playdate app; the host-side half is the **lecturn endpoint binary**. It owns the `spt-usbip-driver` (and companion) Shell instances and is addressable for digests/messages like any endpoint.

**lecturn endpoint binary**:
The `spt-lecturn` adapter's process on a gateway host — the Gateway analogue of a harness binary. Owns: the device link (Playdate comms), the emulated USB device feeding the usbip shell tunnels (crank→hi-res wheel, macros→keyboard/consumer reports), STT, album-art dithering to 1-bit, macro expansion, and **local media sensing** when the target is the gateway host itself. It is the owner-side process: it talks to the Playdate over the device link, to its daemon over the `api` surface, and to target-side shell binaries only through the daemon's shell links/tunnels.
_Avoid_: imagining it "between the broker and shell binaries" — shell binaries live on targets, spawned by their own nodes' brokers.

**gateway host**:
A subnet node whose daemon can host the lecturn endpoint and run the lecturn endpoint binary. Any node with the `spt-lecturn` adapter installed can serve; the endpoint keeps an **instance** per gateway host (see gateway switching). The Playdate is a peripheral of whichever host is active, never a node itself.
_Avoid_: "bridge PC", "the server".

**gateway switching** (ratified 2026-06-11):
The multi-LAN model: ONE lecturn endpoint, an instance on each LAN's gateway host (home, work, …) — the spt-core Instances model verbatim, with the **device link as the driver** that activates an instance. Dock-pair once per gateway host; the app stores each and dials whichever LAN answers. Shells are owned by the endpoint ID (not an instance), so the active instance — wherever it is — drives them. Continuous control therefore never crosses a WAN: you steer machines near you, on the active gateway's LAN.

**device link**:
The Playdate↔gateway-host transport: ONE Playdate-initiated persistent TCP connection (the Playdate's network API is client-shaped and offers no UDP — the device link is never Iroh/QUIC). Up: crank/button state, mic audio, draft edits. Down: digest frames, draft text returned from STT, media info + 1-bit art. USB-serial is the dock-pairing and tethered-fallback path (the `playdial` heritage).
_Avoid_: conflating with the subnet's Iroh links; a second transport for display payloads.

**dock pairing**:
The device-link bootstrap (ratified 2026-06-11): dock the Playdate to a gateway host over USB once; the host pushes `{LAN address, port, pairing token}` into the app; wireless thereafter. The LAN TCP sweep is the cable-free fallback and the silent self-healer when the host's IP moves; crank-entered IP is the escape hatch of last resort. The pairing token doubles as the device-link auth credential.

**gateway sleep/wake** (settled 2026-06-12 — existing spt-core machinery only):
While the endpoint binary runs, lifecycle is **instance state**: ~10min of device-link silence (no Playdate heartbeat = device slept, user gone) flips the instance **dormant**; a Playdate connection flips it **active** (the device link is the driver — the driver-attach rule). Across a node restart, the **gateway host's own usbip shell instance** (present anyway — you steer the gateway host) carries a `wake_command`: its wake-watcher — the one class of binary spt-core boot-launches — holds the device-link port, and a Playdate connection fires the wake opcode → standard wake resolution revives the owner (the endpoint binary spawns, takes over the port) → `persistent` shells come online alongside. Watcher and endpoint binary never run together (the Shell rule, unchanged).

**USB auto-target** (nice-to-have, v1.x — rides the companion):
Physically docking the Playdate to any machine running the companion shell auto-switches scroll/macro targeting to that machine (companion senses the Playdate's USB arrival → sensory event → the Gateway re-aims). On the gateway host itself, the endpoint binary handles the same gesture natively.

### The shells

**spt-usbip-driver (Shell)**:
A **generic USB/IP attach-point Shell** (`kind="shell"` adapter) — any owning endpoint exports an emulated or real USB device to the machine this Shell runs on; the URBs ride a **shell tunnel** (spt-core concept); the target OS attaches the device via its usbip client (usbip-win2 on Windows — signed kernel driver, one-time MSI; kernel usbip on Linux). Its `attach` capability is consent-gated per **(owner × device class × node)** (spt-core per-capability approval gates). Lecturn is the first consumer (a hi-res-wheel mouse + keyboard + consumer-control device), but the Shell is lecturn-agnostic — complex hardware setups can forward arbitrary devices across the subnet.
_Avoid_: "spt-lecturn-driver" (superseded 2026-06-11); adding any sensing to it (it is a dumb attach point — sensing is the companion's job).

**spt-lecturn-companion (Shell, v1.x)**:
The optional target-side **sensing** Shell: emits now-playing media info (track/artist/album/art/status/length/position — GSMTC on Windows, MPRIS on Linux) and, later, focused-app context over the **sensory channel** (ephemeral, REST-only — the verbatim semantic fit for "what's playing now"). Also hosts **named actions**. Targets without it still steer fine; media display stays blank and named actions are unavailable there. Not needed when target == gateway host (the endpoint binary senses locally).

**named action** (ratified 2026-06-12):
A target-side pre-registered action — `name → executor` (an AHK script on Windows, a shell command, anything) authored by the **target's operator on the target**. The wire carries ONLY the name, invoked via the companion shell's command channel behind a per-capability approval gate. The Gateway can pull triggers the target's owner installed; it can never ship executable content. Reconciles "audio-device cycling"-class controls with the keystrokes-only macro rule. AHK = one optional, target-local executor; never a wire payload.
_Avoid_: "remote scripts", wire-supplied script bodies.

**target**:
The machine whose `spt-usbip-driver` Shell instance the lecturn's emulated device is currently attached at. Picked from the system menu's Target options item (the picker lists the Gateway's own shell instances — spt-core's own-instances discovery, nothing new). Switching targets re-aims the device; it does not destroy Shell instances.

### Controls and modes

**machine mode**:
The steering mode: crank = hi-res scroll at the target; macros on buttons; **hold B + crank = volume** (HID Consumer Volume Inc/Dec through the emulated device — works on every target, zero extra software); A reserved for cycling/named actions per config. Media info pane renders when the target senses it (locally on the gateway host; via companion elsewhere).

**macro**:
A named **key sequence or consumer-control usage** (e.g. `ctrl+shift+t`; play/pause/next/prev are single Consumer Page usages — the canonical examples) bound to a Playdate control, expanded **gateway-side** into reports through the emulated device — indistinguishable from typing at the target. Stored in the lecturn adapter's **adapter strings** under **local profiles** (`macros.<name>`), so per-machine/per-context banks ride the profile mechanism. Context-free in v1 (focus-aware macros await the companion); **keystrokes/usages only — never commands** (commands are what named actions are for).

**agent mode**:
The subnet-window mode, two screens:
- **browse screen** — a registry-snapshot tree: **nodes (with their gossiped labels) as parents, endpoints as children**; crank scrolls; up/down moves the cursor; left/right cycles viewed-endpoint history; the highlighted endpoint's **endpoint description** (the registry yellow-pages line) renders beside it; **A** opens its digest; **B** returns to the latest digest.
- **digest screen** — renders the endpoint's session digest (spt-core structured-delta subscription, gateway-rendered for 400×240 1-bit): crank scrolls; **hold A** = speak (mic → device link → gateway STT → draft returns for editing); **A+B** = send (a `user-msg` from the Gateway); **tap B** = backspace one word; **hold B** = clear draft; left/right = cursor; up/down = sent-text history; **tap A** = back to browse, freely — drafts are **persistent per endpoint** (Discord-style: leave and return, the draft is still in the box), so screen-switching never loses words; `playdate.keyboard` (the SDK grid keyboard) = surgical-edit fallback. Chord timing: A+B detection window (~150ms) resolves before the hold-B clear timer arms.

**system menu layout**:
The Playdate MENU button's three custom items (the SDK cap, exactly spent): "Machine Controls" · "Agent Browser" · "Target:" (an options item cycling the shell-instance list).

### Constraints and heritage

**latency budget** (binding constraint, ratified 2026-06-11):
Crank-to-pixel ≤ 25ms p95 on LAN, in both topologies (target == gateway host, target == remote node). Measured by an early spike BEFORE deeper build. Continuous-control traffic never crosses a WAN by design (gateway switching); the shell tunnel is reliable-ordered (no frame drops — congestion = lag), acceptable only inside this LAN bound. If the budget fails on LAN, the recorded escape hatch is a drive-redirect (device streams directly at the current target) — justified only by failed measurements.

**playdial**:
The pre-SPT sibling experiment: a USB-serial reader streaming the Playdate firmware's `buttons` console output (crank float + button bitmasks) to one tethered PC. The device-link protocol heritage; superseded by this project.

## Flagged ambiguities

- **"registers as a proper node"** (early concept notes) — resolved: the lecturn is a *proper endpoint*, never a *node*. PlaydateOS offers no UDP (no QUIC/Iroh) and no std Rust runtime; the nodes are the gateway hosts.
- **speech**: the Playdate's onboard condenser mic captures audio; the device link forwards it to the gateway host for STT — the device never does speech processing.
- **hi-res scrolling without USB/IP**: possible on Linux only (`uinput` speaks `REL_WHEEL_HI_RES` from userspace); on Windows device-grade hi-res requires a real HID device, and usbip-win2 is the least-friction signed path. The project standardizes on USB/IP everywhere rather than splitting backends per OS.

## Example dialogue

> **Dev:** When the user cranks, does the Playdate talk to the target PC?
> **Expert:** Never directly. The crank state rides the **device link** to the active gateway host, where the **lecturn endpoint binary** feeds its emulated USB device; the URBs ride the **shell tunnel** to the **target**'s `spt-usbip-driver` **Shell**, where the OS has the device attached via usbip.
> **Dev:** And the song title on the screen?
> **Expert:** If the target IS the gateway host, the endpoint binary reads it locally (GSMTC/MPRIS) and pushes it down the same device-link socket. A remote target needs the optional **companion** shell, which emits it over the **sensory channel**.
> **Dev:** Cycling the target's audio output — that's a macro?
> **Expert:** No. Macros are keystrokes and consumer usages only. Audio-device cycling has no HID usage — it's a **named action**: the target's owner pre-registered `cycle-audio` on that machine, and the Gateway invokes it by name through the companion, behind a consent gate.
> **Dev:** So installing on a new laptop means installing the Gateway adapter?
> **Expert:** No — the `spt-usbip-driver` Shell adapter (plus its usbip client), and optionally the companion. The Gateway adapter only lives on **gateway hosts**. The laptop becomes a selectable **target** in the menu's Target item.
