Files
Robovoice/Server/PROTOCOL.md
T

5.9 KiB
Raw Blame History

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 <session> <nonce>\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 <session> <nonce>\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 <session> <text>\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 <session> <text>\n

Signals that all segments have been sent. May be empty (HKMSTR:F <session>\n). Triggers playback of all buffered audio on the client. If <text> 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 <text>
  • 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