Jelajahi Sumber

feat(dante): CMC device-announce responder

Add the CMC responder (clean-room, from docs/dante-protocol-notes.md):

- dante_cmc.[hc]: a UDP listener on port 8800 that answers the
  "request device advertisement" message (opcode1 0x1001) with the 22-byte
  DeviceAdvertisement (process id, 8-byte factory device id, IP,
  info-request port). Uses the shared 10-byte big-endian req/resp header:
  echoes start_code/seqnum/opcode1 and sets opcode2 = 1 (OK).
- Runs a small task on core 0 with a 500ms recv timeout for clean stop.
- Kconfig DANTE_CMC_ON_BOOT (default n); main starts it (before advertise)
  when enabled, reusing the same identity built from MAC + IP.

Lets a peer that found us via mDNS resolve our full identity. Still needs
ARC (routing/channel queries) to be fully usable from Dante Controller.
Not build-verified; untested vs hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Benjamin Harris 3 bulan lalu
induk
melakukan
14421b5bea

+ 2 - 1
README.md

@@ -112,7 +112,8 @@ toolchain.
 - [x] Prototype Dante mDNS **discovery** (`components/dante`, read-only browse)
 - [x] Prototype Dante mDNS **advertisement** (`_netaudio-arc`/`-cmc`/`-chan`;
       gated off until ARC/CMC responders back it)
-- [ ] Prototype Dante control protocol (routing/subscriptions)
+- [x] Prototype Dante **CMC** responder (device-announce, UDP 8800)
+- [ ] Prototype Dante **ARC** control protocol (routing/subscriptions, UDP 4440)
 - [ ] Investigate PTPv1 support alongside the existing PTPv2 stack
 
 ## License

+ 1 - 0
components/dante/CMakeLists.txt

@@ -2,6 +2,7 @@ idf_component_register(
     SRCS "src/dante_discovery.c"
          "src/dante_device.c"
          "src/dante_advertise.c"
+         "src/dante_cmc.c"
     INCLUDE_DIRS "include"
     REQUIRES esp_netif
     PRIV_REQUIRES mdns lwip

+ 9 - 0
components/dante/Kconfig

@@ -28,4 +28,13 @@ config DANTE_ADVERTISE_ON_BOOT
         cannot answer queries shows up broken in Dante Controller.
         PROTOTYPE / untested against real hardware.
 
+config DANTE_CMC_ON_BOOT
+    bool "Run the Dante CMC (device-announce) responder"
+    default n
+    help
+        Start the CMC responder, which answers device-advertisement requests
+        on UDP 8800 with this device's identity. Needed for a peer to resolve
+        our full identity after finding us via mDNS. Still incomplete without
+        the ARC responder (routing/channel queries). PROTOTYPE / untested.
+
 endmenu

+ 15 - 3
components/dante/README.md

@@ -47,6 +47,19 @@ can't answer ARC/CMC makes it show up *broken* in Dante Controller, so only
 turn it on once the responders below exist. TXT version strings mimic
 real-hardware values and are prototype/untested — verify against a capture.
 
+### CMC responder (`dante_cmc.h`)
+
+Answers the "request device advertisement" message on UDP 8800: a peer that
+found us via mDNS sends it to resolve our full identity, and we reply with the
+process id, 8-byte factory device id, IP, and info-request port. Uses the
+10-byte big-endian ARC-style request/response header (echo start_code/seqnum/
+opcode1, set opcode2 = 1, opcode1 `0x1001`).
+
+- `dante_cmc_start(id)` / `dante_cmc_stop()` — runs a small UDP listener task.
+
+**Gated OFF by default** (`DANTE_CMC_ON_BOOT`). Still incomplete without ARC.
+PROTOTYPE / untested against hardware — verify against a capture.
+
 #### Enabling
 
 Controlled by Kconfig (`idf.py menuconfig` → *Dante*):
@@ -57,10 +70,9 @@ Controlled by Kconfig (`idf.py menuconfig` → *Dante*):
 
 ## Not yet implemented
 
-Everything past discovery + advertisement, in rough dependency order (see the
-protocol notes):
+Everything past discovery + advertisement + CMC, in rough dependency order
+(see the protocol notes):
 
-- **CMC** server — device-announce request/response (port 8800).
 - **ARC** server — capability + channel enumeration and the name-based
   subscription (routing) model (port 4440).
 - **Flow control (dbcp)** — open/update/stop audio flows with keepalives

+ 38 - 0
components/dante/include/dante_cmc.h

@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Dante CMC (device-announce) responder — clean-room, from
+ * docs/dante-protocol-notes.md.
+ *
+ * Listens on the CMC UDP port (8800) and answers the "request device
+ * advertisement" message with this device's identity (process id, 8-byte
+ * factory device id, IP, info-request port). This is how a peer that found us
+ * via mDNS bootstraps to our full identity.
+ *
+ * Uses the same 10-byte big-endian request/response header as ARC:
+ *   start_code(u16) total_length(u16) seqnum(u16) opcode1(u16) opcode2(u16)
+ * A request has opcode2 == 0; the reply echoes start_code/seqnum/opcode1 and
+ * sets opcode2 = 1 (OK), with the advertisement as the content.
+ *
+ * PROTOTYPE / UNTESTED against real hardware — verify against a capture.
+ */
+#pragma once
+
+#include "dante_device.h"
+#include "esp_err.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Start the CMC responder task. Binds UDP on id->cmc_port and answers
+ * device-advertisement requests using `id` (copied internally). */
+esp_err_t dante_cmc_start(const dante_identity_t *id);
+
+/* Stop the CMC responder task and close its socket. */
+void dante_cmc_stop(void);
+
+#ifdef __cplusplus
+}
+#endif

+ 187 - 0
components/dante/src/dante_cmc.c

@@ -0,0 +1,187 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Dante CMC (device-announce) responder — clean-room, from
+ * docs/dante-protocol-notes.md.
+ */
+
+#include "dante_cmc.h"
+
+#include <string.h>
+#include <errno.h>
+#include <sys/time.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_log.h"
+#include "lwip/sockets.h"
+
+static const char *TAG = "dante_cmc";
+
+/* req/resp framing (shared with ARC). */
+#define CMC_HEADER_LEN 10
+#define CMC_CODE_OK     1
+#define CMC_OP_REQUEST_DEVICE_ADVERTISEMENT 0x1001
+
+/* DeviceAdvertisement content is 22 bytes; full reply = header + content. */
+#define CMC_ADV_CONTENT_LEN 22
+#define CMC_REPLY_LEN (CMC_HEADER_LEN + CMC_ADV_CONTENT_LEN)
+
+#define CMC_RECV_BUF 512
+
+static dante_identity_t s_id;
+static volatile bool    s_running;
+static int              s_sock = -1;
+static TaskHandle_t     s_task;
+
+/* Big-endian field helpers (wire format is big-endian; the P4 is little). */
+static inline uint16_t rd_u16(const uint8_t *p)
+{
+    return (uint16_t)((p[0] << 8) | p[1]);
+}
+static inline void wr_u16(uint8_t *p, uint16_t v)
+{
+    p[0] = (uint8_t)(v >> 8);
+    p[1] = (uint8_t)(v & 0xFF);
+}
+
+/* Build the DeviceAdvertisement content (22 bytes, big-endian) into `out`. */
+static void build_advertisement(uint8_t *out)
+{
+    wr_u16(&out[0], s_id.process_id);
+    memcpy(&out[2], s_id.device_id, DANTE_DEVICE_ID_LEN);  /* [2..9] */
+    wr_u16(&out[10], 1);                                   /* unknown, observed = 1 */
+    wr_u16(&out[12], 0);                                   /* unknown, observed = 0 */
+    out[14] = (uint8_t)((s_id.ipv4 >> 24) & 0xFF);         /* IP a.b.c.d */
+    out[15] = (uint8_t)((s_id.ipv4 >> 16) & 0xFF);
+    out[16] = (uint8_t)((s_id.ipv4 >> 8) & 0xFF);
+    out[17] = (uint8_t)(s_id.ipv4 & 0xFF);
+    wr_u16(&out[18], s_id.info_port);
+    wr_u16(&out[20], 0);                                   /* unknown, observed = 0 */
+}
+
+static void handle_request(int sock, const uint8_t *req, int len,
+                           const struct sockaddr_in *src, socklen_t src_len)
+{
+    if (len < CMC_HEADER_LEN) {
+        return;  /* too short to be a valid message */
+    }
+
+    uint16_t start_code = rd_u16(&req[0]);
+    uint16_t seqnum     = rd_u16(&req[4]);
+    uint16_t opcode1    = rd_u16(&req[6]);
+    uint16_t opcode2    = rd_u16(&req[8]);
+
+    /* opcode2 != 0 marks a response, not a request — ignore. */
+    if (opcode2 != 0) {
+        return;
+    }
+    if (opcode1 != CMC_OP_REQUEST_DEVICE_ADVERTISEMENT) {
+        ESP_LOGD(TAG, "unhandled CMC opcode1 0x%04x", opcode1);
+        return;
+    }
+
+    uint8_t reply[CMC_REPLY_LEN];
+    /* Echo start_code / seqnum / opcode1; opcode2 = OK. */
+    wr_u16(&reply[0], start_code);
+    wr_u16(&reply[2], CMC_REPLY_LEN);
+    wr_u16(&reply[4], seqnum);
+    wr_u16(&reply[6], opcode1);
+    wr_u16(&reply[8], CMC_CODE_OK);
+    build_advertisement(&reply[CMC_HEADER_LEN]);
+
+    int sent = sendto(sock, reply, sizeof(reply), 0,
+                      (const struct sockaddr *)src, src_len);
+    if (sent < 0) {
+        ESP_LOGW(TAG, "sendto failed: errno %d", errno);
+    } else {
+        ESP_LOGD(TAG, "answered device-advertisement request");
+    }
+}
+
+static void cmc_task(void *arg)
+{
+    (void)arg;
+
+    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock < 0) {
+        ESP_LOGE(TAG, "socket() failed: errno %d", errno);
+        s_task = NULL;
+        vTaskDelete(NULL);
+        return;
+    }
+
+    /* 500ms recv timeout so the loop can observe s_running for a clean stop. */
+    struct timeval tv = { .tv_sec = 0, .tv_usec = 500 * 1000 };
+    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+    struct sockaddr_in bind_addr = {
+        .sin_family = AF_INET,
+        .sin_port   = htons(s_id.cmc_port),
+        .sin_addr   = { .s_addr = htonl(INADDR_ANY) },
+    };
+    if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
+        ESP_LOGE(TAG, "bind(:%u) failed: errno %d", s_id.cmc_port, errno);
+        close(sock);
+        s_task = NULL;
+        vTaskDelete(NULL);
+        return;
+    }
+
+    s_sock = sock;
+    ESP_LOGI(TAG, "CMC responder listening on udp/%u", s_id.cmc_port);
+
+    uint8_t buf[CMC_RECV_BUF];
+    while (s_running) {
+        struct sockaddr_in src;
+        socklen_t src_len = sizeof(src);
+        int n = recvfrom(sock, buf, sizeof(buf), 0,
+                         (struct sockaddr *)&src, &src_len);
+        if (n < 0) {
+            if (errno == EAGAIN || errno == EWOULDBLOCK) {
+                continue;  /* timeout — re-check s_running */
+            }
+            if (s_running) {
+                ESP_LOGW(TAG, "recvfrom failed: errno %d", errno);
+            }
+            break;
+        }
+        handle_request(sock, buf, n, &src, src_len);
+    }
+
+    s_sock = -1;
+    close(sock);
+    ESP_LOGI(TAG, "CMC responder stopped");
+    s_task = NULL;
+    vTaskDelete(NULL);
+}
+
+esp_err_t dante_cmc_start(const dante_identity_t *id)
+{
+    if (id == NULL) {
+        return ESP_ERR_INVALID_ARG;
+    }
+    if (s_task != NULL) {
+        ESP_LOGW(TAG, "CMC responder already running");
+        return ESP_ERR_INVALID_STATE;
+    }
+    s_id = *id;
+    s_running = true;
+
+    /* Core 0 for coherency with the net/audio tasks; modest priority — CMC is
+     * control-plane and low-rate, must never contend with the audio path. */
+    BaseType_t ok = xTaskCreatePinnedToCore(cmc_task, "dante_cmc",
+                                            4096, NULL, 5, &s_task, 0);
+    if (ok != pdPASS) {
+        s_running = false;
+        s_task = NULL;
+        ESP_LOGE(TAG, "failed to create CMC task");
+        return ESP_ERR_NO_MEM;
+    }
+    return ESP_OK;
+}
+
+void dante_cmc_stop(void)
+{
+    s_running = false;  /* task wakes on its recv timeout and self-deletes */
+}

+ 10 - 3
main/main.c

@@ -41,6 +41,7 @@
 #include "aes67_webui.h"
 #include "dante_discovery.h"
 #include "dante_advertise.h"
+#include "dante_cmc.h"
 #include "esp_mac.h"
 #include "dsps_tone_gen.h"
 #include "driver/i2c.h"
@@ -661,9 +662,10 @@ void app_main(void)
     dante_discovery_start(CONFIG_DANTE_DISCOVERY_INTERVAL_MS, NULL, NULL);
 #endif
 
-#if CONFIG_DANTE_ADVERTISE_ON_BOOT
-    /* Build a Dante identity from this board's MAC + IP and advertise it over
-     * mDNS. PROTOTYPE — only meaningful once the CMC/ARC responders exist. */
+#if CONFIG_DANTE_ADVERTISE_ON_BOOT || CONFIG_DANTE_CMC_ON_BOOT
+    /* Build a Dante identity from this board's MAC + IP, then bring up the
+     * advertisement and/or CMC responder. PROTOTYPE — the device is only
+     * fully usable from Dante Controller once ARC also exists. */
     {
         static dante_identity_t s_dante_id;
         uint8_t mac[6] = {0};
@@ -681,7 +683,12 @@ void app_main(void)
             ((uint32_t)esp_ip4_addr4_16(&ip_info.ip));
 
         dante_identity_init_default(&s_dante_id, ipv4, mac);
+#if CONFIG_DANTE_CMC_ON_BOOT
+        dante_cmc_start(&s_dante_id);        /* answer queries before we advertise */
+#endif
+#if CONFIG_DANTE_ADVERTISE_ON_BOOT
         dante_advertise_start(&s_dante_id);
+#endif
     }
 #endif