Преглед на файлове

docs: fill in clean-room Dante protocol notes

Behavioural notes derived from reading reference/inferno (read-only, per
the CLAUDE.md licensing rule) and the public dante-aes67-relay.js gist —
written independently, in our own words, not translated from source:

- UDP control services and standard ports (ARC 4440, CMC 8800,
  dbcp/flow-control 4455, info 8700, mDNS, media ~5004).
- mDNS discovery: _netaudio-arc/-cmc/-chan/-bund service types + TXT keys.
- ARC control: 10-byte req/resp header, opcode catalogue, pagination,
  and the name-based subscription (routing) model.
- Flow setup (dbcp): request/update/stop flows, 6-byte handle,
  keepalive-or-expire behaviour, error codes.
- Media framing (strip 9 bytes -> interleaved PCM) and RTP timestamp
  reconstruction; phase-1 AES67-mode vs phase-2 native-Dante split.
- Clocking: PTPv1 (legacy) vs PTPv2 (AES67), tied to known blockers.

Byte-exact struct layouts intentionally not transcribed; to be
re-derived from packet captures so the implementation stays independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Benjamin Harris преди 3 месеца
родител
ревизия
b0c90fda7c
променени са 1 файла, в които са добавени 239 реда и са изтрити 6 реда
  1. 239 6
      docs/dante-protocol-notes.md

+ 239 - 6
docs/dante-protocol-notes.md

@@ -8,14 +8,247 @@ file.
 Write what a Dante device *does*, in our own words (message purpose, timing,
 field meanings) — not how Inferno's code does it.
 
-## Discovery (mDNS)
+> **Status:** first pass, derived from reading Inferno (an unofficial,
+> reverse-engineered implementation) plus the public
+> `dante-aes67-relay.js` gist. Dante is proprietary and undocumented;
+> everything here is reverse-engineered and partly guessed. **Confirm every
+> value against a real packet capture before implementing.** Byte-exact
+> struct layouts are deliberately *not* transcribed here — capture them fresh
+> when writing the code, so the implementation is genuinely independent.
 
-TBD
+## Big picture
 
-## Control protocol (routing / subscriptions)
+A Dante device exposes several independent UDP services and advertises them
+over mDNS. Discovery and control are separate from the audio itself:
 
-TBD
+- **Discovery** — mDNS/DNS-SD advertises the device and each of its channels.
+- **Control (ARC)** — a controller (e.g. Dante Controller) or a peer device
+  queries capabilities, channel lists, and *routing* (which TX channel feeds
+  which RX channel) over a request/response UDP protocol.
+- **Flow setup** — before audio moves, a receiver asks a transmitter to open
+  an audio "flow" (a bundle of channels to a destination socket), which must
+  then be kept alive.
+- **Media transport** — the actual PCM audio, unicast or multicast, in
+  Dante's own lightweight framing (close to, but not, AES67 RTP).
+- **Clocking** — PTP keeps every device sample-accurate. Legacy Dante uses
+  **PTPv1**; AES67 mode uses **PTPv2**.
 
-## Clocking (PTPv1)
+Names Inferno uses (worth knowing because captures/tools use them too): the
+control protocol is **ARC**, device discovery/announce is **CMC**, and the
+media clock control protocol is **dbcp**.
 
-TBD
+### UDP ports (standard, as seen on hardware)
+
+| Service | Port | Role |
+|---|---|---|
+| ARC (control / routing) | 4440 | capability + channel queries, subscriptions |
+| CMC (device announce) | 8800 | device-advertisement request/response |
+| Flow control (dbcp) | 4455 | open/update/stop audio flows |
+| Info / multicast | 8700 | channel-change notifications, info requests |
+| mDNS | 5353 | discovery |
+| Media (RTP-ish) | 5004 (default) | PCM audio; actual port is negotiated |
+
+Multiple instances on one IP shift the first four ports by an offset (Dante
+Controller and peers learn the real ports from mDNS TXT / CMC, so they don't
+have to be the standard ones). Media source ports in particular are
+OS-allocated and only knowable from the flow-setup exchange.
+
+All control messages are **big-endian**.
+
+## Discovery (mDNS / DNS-SD)
+
+Everything is under `_udp.local`. A device publishes several service types:
+
+- **`_netaudio-arc._udp`** — the device's control endpoint. Points at the ARC
+  port. TXT keys include `arcp_vers` / `arcp_min` (ARC protocol version and
+  minimum), `router_vers`, `router_info` (board name), `mf` (manufacturer),
+  `model`. Advertised with a long TTL (~4500 s).
+- **`_netaudio-cmc._udp`** — the device announce endpoint (CMC port). TXT keys
+  include `id` (the 8-byte *factory device ID* as hex — typically the MAC
+  padded to 8 bytes), `process` (process ID, distinguishes multiple instances
+  on one IP), `cmcp_vers`/`cmcp_min`, `server_vers`, a `channels` bitfield,
+  `mf`, `model`.
+- **`_netaudio-chan._udp`** — **one instance per TX channel**. The service
+  name is `<channelname>@<hostname>`. Points at the flow-control port. TXT
+  keys describe how to receive that channel: `txtvers`, `dbcp1` (flow-control
+  start code, e.g. `0x1102`), `dbcp`, `id` (1-based channel index), `rate`
+  (sample rate), `pcm` (bytes-per-sample + PCM type), `enc`/`en` (bit depth),
+  `latency_ns`, `fpp` (frames-per-packet range advertised as `max,min`),
+  `nchan` (max channels per flow), and `default` on the factory-name copy.
+  A channel with a user-assigned friendly name is advertised twice (factory
+  name and friendly name).
+- **`_netaudio-bund._udp`** — a **multicast bundle** (a pre-made multicast
+  flow other devices can join). TXT keys: `id`, `nchan`, `latency_ns`, `fpp`,
+  `rate`, `enc`, and crucially `a.0` (destination multicast IP) and `p.0`
+  (destination port) — so a receiver can subscribe to a multicast without a
+  unicast flow-setup handshake.
+
+So a controller learns the whole routing picture from mDNS alone: which
+devices exist, what channels each transmits, and how to reach them.
+
+> Inferno also publishes an `in-addr.local` record with an
+> `_inferno-response-origin` TXT to reserve a multicast address; that part is
+> Inferno-specific bookkeeping, not core Dante behaviour.
+
+## Control protocol (ARC — routing / subscriptions)
+
+Request/response over UDP on the ARC port (4440). Every message starts with a
+**10-byte header** followed by a content blob:
+
+```text
+u16 start_code     # protocol/version marker
+u16 total_length   # header + content
+u16 seqnum         # echoed in the response, matches replies to requests
+u16 opcode1        # which operation
+u16 opcode2        # in a request: 0; in a response: status (1 = OK)
+... content ...
+```
+
+The responder echoes `start_code`, `seqnum`, and `opcode1`, and sets
+`opcode2` to a status: **1 = OK**. A response code of **`0x8112`** means "more
+results exist" — large lists (channel enumerations) are **paginated**: the
+request carries a 1-based start index and the reply signals whether to ask
+again. Other non-1 codes are errors.
+
+### Operations (opcode1 values observed)
+
+| opcode1 | Operation | Purpose |
+|---|---|---|
+| `0x1000` | channel & flow counts | capability probe: TX/RX channel counts, max channels per flow, max TX/RX flows, feature flags (supports TX rename, supports TX multicast) |
+| `0x1002` / `0x1003` | device names | board name, revision, friendly + factory hostname |
+| `0x2000` | get TX channels | enumerate transmit channels (id, name, format) |
+| `0x2010` | TX channel friendly names | friendly-name overrides for TX channels |
+| `0x2013` | rename TX channel | set a TX channel's friendly name |
+| `0x3000` | get RX channels | enumerate receive channels **incl. subscription status** |
+| `0x3001` | rename RX channel | set an RX channel's friendly name |
+| `0x3010` | **set channel subscriptions** | the routing operation — see below |
+| `0x2200` | query TX flows | list active transmit flows |
+| `0x2201` | create multicast TX flow | make a new multicast flow |
+| `0x2202` | delete multicast TX flow | tear one down |
+| `0x3200` | query RX flows | list active receive flows |
+
+### The subscription (routing) model
+
+Routing in Dante is expressed **from the receiver's side**. To route
+"channel *X* of device *Y*" into local RX channel *Z*, the controller sends a
+`set channel subscriptions` (`0x3010`) request naming, per subscription:
+
+- the **local RX channel id**,
+- the **TX channel name** (string), and
+- the **TX (source) hostname** (string).
+
+i.e. subscriptions are by *name*, not by IP/port — the device then resolves
+the name via mDNS and negotiates the actual flow itself. When you later
+enumerate RX channels (`0x3000`), each carries a **subscription status** word
+(distinct values for "subscribed & receiving", "pending / unresolved", etc.).
+
+Channel formats are described by a common descriptor carrying **sample rate**,
+**bit depth**, and a **PCM type** code (e.g. L16/L24-style). These reappear in
+mDNS TXT (`rate`, `enc`, `pcm`) and in flow setup — they must all agree or
+flow setup fails with a sample-rate-mismatch error.
+
+### CMC — device announce (port 8800)
+
+A short request (`0x1001`, "request device advertisement") elicits an
+advertisement carrying the **process id**, the **8-byte factory device id**,
+the device's **IP**, and the **info-request port**. This is how a peer bootstraps
+from "I saw you in mDNS" to "here's your identity and where to send info
+requests."
+
+### Multicast info / channel-change (port 8700)
+
+A wider **32-byte header** (start code, total length, seqnum, process id,
+8-byte factory device id, 8-byte vendor string, 8-byte opcode, then content).
+The notable message is a **channel-change notification**: start code `0xffff`,
+a fixed opcode, and content that is a **bitmask of channel indices** that
+changed (so listeners know to re-query rather than poll). Used for pushing
+"something about my channels changed" to interested parties.
+
+## Flow setup (dbcp — port 4455)
+
+Before audio flows unicast, the receiver asks the transmitter to open a
+**flow**. Same 10-byte req/resp header as ARC, but the start code is the
+`dbcp1` value from mDNS (e.g. `0x1102`). Operations (in `opcode1`):
+
+- **`0x0100` request flow** — "send me these channels." Content includes
+  sample rate, bit depth, **frames-per-packet (fpp)**, the ordered list of TX
+  channel ids wanted (0 = gap/unused slot), and the receiver's socket
+  (address + port) and flow name. The reply returns a **6-byte flow handle**.
+- **`0x0102` update flow** — change the channel list of an existing flow (by
+  handle).
+- **`0x0101` stop flow** — tear down a flow (by handle).
+
+**Flows expire without keepalives** — a flow that goes quiet is dropped and
+further packets to it return a "flow not found / expired" error. So a receiver
+must periodically refresh. Known error codes (in `opcode2` of the reply):
+
+| code | meaning |
+|---|---|
+| `0x0103` | flow expired / not found (missing keepalives) |
+| `0x0315` | too many TX flows |
+| `0x0301` | sample-rate mismatch |
+
+Multicast flows are simpler for the receiver: the transmitter advertises the
+bundle via `_netaudio-bund` mDNS (with destination IP/port in TXT), so a
+receiver can just join the multicast group instead of doing a unicast
+request-flow handshake.
+
+## Media transport (the audio itself)
+
+Dante media packets are **UDP carrying PCM**, deliberately close to AES67 RTP
+but with a lighter, Dante-specific header. From the public relay gist, a Dante
+multicast audio packet looks like:
+
+```text
+offset 0  : u8   channel count
+offset 1  : u32  timestamp, seconds (big-endian)
+offset 5  : u16  (reserved, seen as 0)
+offset 7  : u16  media timestamp — sub-second sample offset (big-endian)
+offset 9  : PCM samples, interleaved, big-endian (L16 / L24 / L32)
+```
+
+That is: **strip the first 9 bytes of the UDP payload and the rest is raw
+interleaved PCM.** The RTP timestamp an AES67 receiver expects is
+reconstructable as `seconds * sample_rate + media_offset` (mod 2^32).
+
+Practical consequences for us:
+
+- **AES67 interop path (phase 1):** we already speak AES67 RTP on 5004. A
+  Dante device with **AES67 mode enabled** emits standard RTP we can consume
+  directly — no Dante control protocol needed. This is the fastest route to
+  "hears Dante audio."
+- **Native Dante path (phase 2):** to talk to non-AES67 Dante devices we'd
+  need to (a) advertise/answer mDNS as above, (b) implement enough ARC to be
+  discoverable and subscribable, (c) do dbcp flow setup + keepalives, and
+  (d) translate the 9-byte-header media framing to/from our I2S engine. The
+  media conversion itself is cheap; the control/discovery surface is the work.
+
+Dante multicast-only AES67 in Dante gear is locked to **48 kHz**; native Dante
+flows support other rates negotiated via the descriptors above.
+
+## Clocking (PTP)
+
+- **Legacy / default Dante: PTPv1** (IEEE 1588-2002). This is the blocker
+  noted in `CLAUDE.md` — the forked AES67 stack does PTPv2 against the P4's
+  hardware-timestamp path, and PTPv1 differs on the wire (message formats,
+  no two-step/native support in the same code path).
+- **AES67 mode: PTPv2** (IEEE 1588-2008, AES67 media profile). If the Dante
+  gear can be put in AES67 mode, our existing PTPv2 works and PTPv1 is not
+  needed for phase 1.
+- A device can be a PTP **follower** (slaves to a grandmaster) or, harder, a
+  **leader/grandmaster**. Inferno relies on an external PTP daemon (Statime)
+  as a separate OS process — an option we do **not** have on ESP-IDF, so any
+  PTPv1 work must live inside the firmware's existing PTP task (see
+  `CLAUDE.md` "No multi-process model").
+
+## Open questions / to verify against captures
+
+- Exact byte layout of ARC channel/flow descriptors (offsets, the several
+  still-unknown fields) — re-derive from captures when implementing.
+- `pcm_type` code values ↔ AES67 codec names (L16 / L24 / L32 / AM824).
+- How subscription-status words map to Dante Controller's coloured states.
+- Whether our device must answer CMC/ARC to merely *receive* multicast, or
+  only to be *routable from Dante Controller*. (Likely the latter — multicast
+  receive may be possible from mDNS bundle info alone.)
+- PTPv1 message specifics and how badly they diverge from the existing v2
+  path.