# Robovoice DHCP Tunnel Protocol ## Overview Robovoice communicates with a remote STT server by tunneling through the DHCP UDP ports (68→67). This exploits a common killswitch exception: VPN software (e.g. WireGuard) blocks all traffic except DHCP, which is allowed for network connectivity maintenance. ``` [Robovoice client] --broadcast UDP :68→:67--> [STT server] [Robovoice client] <--unicast UDP :67→:68-- [STT server] ``` The client broadcasts NOP heartbeats while PTT is held. The server starts recording on the first NOP and stops when it receives OFF or when 150ms pass with no NOPs. ## Transport - **Protocol:** UDP (connectionless, unreliable) - **Client → Server:** broadcast, source port 68, dest port 67 - **Server → Client:** unicast, source port 67, dest port 68 - **Client binds:** to a specific LAN interface IP on port 68 (with `SO_REUSEADDR` to coexist with the Windows DHCP service) - **No connection state** — purely fire-and-forget datagrams ## Wire format All messages are plain text, newline-terminated (`\n`). Every message starts with the 6-byte magic `HKMSTR` to distinguish our traffic from real DHCP. ### Client → Server **NOP (heartbeat while PTT held):** ``` HKMSTR \n ``` Sent every 50ms while PTT is held. The session is an incrementing unsigned integer that identifies the current PTT utterance (incremented on each PTT press). The nonce is an incrementing unsigned integer that makes each datagram unique. The server should echo the session back in replies. Both are discarded by the server for protocol logic — the server tracks liveness via "did anything arrive recently." **OFF (PTT released):** ``` HKMSTR:OFF \n ``` Sent once when PTT is released. This is the fast-stop signal. If lost, the 150ms timeout acts as a backstop. ### Server → Client The server echoes the session ID from the NOPs in all replies. The client drops any reply with a stale session ID. **Partial segment (completed VAD segment):** ``` HKMSTR:P \n ``` A completed, VAD-separated utterance segment. The client starts TTS synthesis immediately and buffers the audio output, but does **not** play it yet. Playback starts when `:F` arrives (or timeout). **Final (all done):** ``` HKMSTR:F \n ``` Signals that all segments have been sent. May be empty (`HKMSTR:F \n`). Triggers playback of all buffered audio on the client. If `` is non-empty, the client synthesizes it before playing. The purpose of this design is to minimize latency: TTS synthesis runs in parallel with recording, so by the time `:F` arrives, audio is already buffered and playback starts immediately. ## Server state machine ``` ┌──────────────────────────────────────────┐ │ │ ▼ │ ┌──────────┐ first NOP ┌──────────────┐ │ │ IDLE │ ──────────► │ RECORDING │ │ └──────────┘ └──────────────┘ │ │ │ │ OFF │ │ 150ms │ recv'd │ │ silence │ ▼ ▼ │ ┌─────────────┐ │ │ PROCESSING │ │ └─────────────┘ │ │ │ send │ │ :P/:F │ │ ▼ │ back to IDLE ──────────┘ ``` - **IDLE → RECORDING:** first NOP received, start mic capture - **RECORDING:** VAD detects completed segments → send `HKMSTR:P ` - **RECORDING → PROCESSING:** OFF received, OR 150ms since last NOP - **PROCESSING → IDLE:** send remaining segments as `:P`, then `HKMSTR:F` ## Client playback model 1. `:P` arrives → start TTS synthesis immediately, buffer audio (don't play). Reset 250ms segment timer. 2. More `:P` arrive → keep synthesizing and buffering, reset timer each time. 3. `:F` arrives → play all buffered audio immediately, cancel timer. 4. If `:F` doesn't arrive within 250ms of the last `:P` → play buffered audio early. If more `:P` arrive after early playback, synthesis continues and new audio is appended to the output — not a failure. 5. `:F` may be empty — it just signals "all segments sent, start/confirm playback." ## Timing | Parameter | Value | Purpose | |-----------|-------|---------| | NOP interval | 50ms | Heartbeat frequency while PTT held | | Silence timeout | 150ms | Stop recording if no NOPs (3 missed = lost OFF) | | NOP bandwidth | ~20 msg/s × ~20 bytes | ~400 bytes/s — negligible | ## Why this works 1. **Outbound broadcast `:68→:67` to `255.255.255.255`** passes the WireGuard WFP killswitch (DHCP exception matches this exact pattern) 2. **Inbound `:67→:68`** has no address restriction in the WFP rule, so unicast replies pass through 3. **Binding to a specific interface IP** (not `0.0.0.0`) wins unicast delivery over the Windows DHCP client service 4. **NOP spam** ensures the ON message gets through even at 5% packet loss (3 consecutive NOPs = ~0.01% drop probability) 5. **150ms timeout** is the backstop for lost OFF — at 50ms intervals, 3 consecutive NOPs must all be lost to false-stop