Explorar el Código

feat(dante): mDNS advertisement + device identity

Add the advertisement half of Dante discovery (clean-room, from
docs/dante-protocol-notes.md):

- dante_device.[hc]: shared dante_identity_t (hostname, 8-byte device id,
  control ports, mf/model, audio format) + dante_identity_init_default()
  that derives it from the board MAC + IP.
- dante_advertise.[hc]: publish _netaudio-arc and _netaudio-cmc device
  records, plus _netaudio-chan per TX channel, via the ESP-IDF mdns
  component. TXT version strings mimic real-hardware values (one place to
  tune). start/stop remove only Dante service types, not AES67's.
- Kconfig DANTE_ADVERTISE_ON_BOOT (default n); main builds an identity and
  advertises when enabled.

Gated OFF by default: an advertised device that can't answer ARC/CMC shows
up broken in Dante Controller, so this only becomes useful once the CMC
(next) and ARC responders exist. Not build-verified; untested vs hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Benjamin Harris hace 3 meses
padre
commit
d1e716e71c

+ 2 - 1
README.md

@@ -110,7 +110,8 @@ toolchain.
 - [ ] Get AES67 send/receive verified against real Dante gear in AES67 mode
 - [x] Write clean-room Dante protocol notes (`docs/dante-protocol-notes.md`)
 - [x] Prototype Dante mDNS **discovery** (`components/dante`, read-only browse)
-- [ ] Prototype Dante mDNS **advertisement** (needs ARC/CMC servers behind it)
+- [x] Prototype Dante mDNS **advertisement** (`_netaudio-arc`/`-cmc`/`-chan`;
+      gated off until ARC/CMC responders back it)
 - [ ] Prototype Dante control protocol (routing/subscriptions)
 - [ ] Investigate PTPv1 support alongside the existing PTPv2 stack
 

+ 2 - 0
components/dante/CMakeLists.txt

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

+ 10 - 0
components/dante/Kconfig

@@ -18,4 +18,14 @@ config DANTE_DISCOVERY_INTERVAL_MS
     help
         How often the background scanner repeats the mDNS sweep.
 
+config DANTE_ADVERTISE_ON_BOOT
+    bool "Advertise this device over mDNS (Dante)"
+    default n
+    help
+        Publish _netaudio-arc / _netaudio-cmc mDNS records so this device
+        appears in Dante Controller. Leave OFF until the control-protocol
+        responders (CMC, then ARC) are running — an advertised device that
+        cannot answer queries shows up broken in Dante Controller.
+        PROTOTYPE / untested against real hardware.
+
 endmenu

+ 19 - 4
components/dante/README.md

@@ -30,6 +30,23 @@ interop working.
 It shares the process-wide ESP-IDF mDNS instance with the AES67 stack (it only
 ever queries; it never frees mDNS).
 
+### Advertisement (`dante_advertise.h`, `dante_device.h`)
+
+Publishes this device over mDNS so Dante Controller / peers can see it:
+`_netaudio-arc` (control endpoint) and `_netaudio-cmc` (announce endpoint) at
+the device level, plus `_netaudio-chan` per transmit channel. Driven by a
+`dante_identity_t` (hostname, 8-byte device id, ports, manufacturer/model,
+audio format), which `dante_identity_init_default()` fills from the board's
+MAC + IP.
+
+- `dante_advertise_start(id)` / `dante_advertise_stop()`
+- `dante_advertise_tx_channel(index, name)`
+
+**Gated OFF by default** (`DANTE_ADVERTISE_ON_BOOT`). Advertising a device that
+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.
+
 #### Enabling
 
 Controlled by Kconfig (`idf.py menuconfig` → *Dante*):
@@ -40,11 +57,9 @@ Controlled by Kconfig (`idf.py menuconfig` → *Dante*):
 
 ## Not yet implemented
 
-Everything past discovery, in rough dependency order (see the protocol notes):
+Everything past discovery + advertisement, in rough dependency order (see the
+protocol notes):
 
-- **Advertisement** — publishing `_netaudio-arc`/`-cmc`/`-chan`/`-bund` so
-  Dante Controller sees this device. Deliberately not done yet: an advertised
-  device that can't answer ARC/CMC queries is worse than an invisible one.
 - **CMC** server — device-announce request/response (port 8800).
 - **ARC** server — capability + channel enumeration and the name-based
   subscription (routing) model (port 4440).

+ 53 - 0
components/dante/include/dante_advertise.h

@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Dante mDNS advertisement — clean-room, from docs/dante-protocol-notes.md.
+ *
+ * Publishes this device's presence over mDNS/DNS-SD so Dante Controller and
+ * peer devices can see it. Advertises the device-level services:
+ *   _netaudio-arc._udp   (control endpoint, ARC port)
+ *   _netaudio-cmc._udp   (announce endpoint, CMC port)
+ * and, per transmit channel, _netaudio-chan._udp.
+ *
+ * WARNING: advertising is only half the story. A device that appears in
+ * discovery but cannot answer ARC/CMC queries is worse than an invisible one
+ * (it shows up broken in Dante Controller). Only enable this once the CMC and
+ * ARC responders are running. It is gated OFF by default in Kconfig.
+ *
+ * PROTOTYPE / UNTESTED: TXT version strings mimic values observed on real
+ * Dante hardware and may need tuning against a specific Dante Controller
+ * version. Verify against a capture.
+ */
+#pragma once
+
+#include "dante_device.h"
+#include "esp_err.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Start advertising the device-level Dante services (_netaudio-arc and
+ * _netaudio-cmc) from `id`. Sets the mDNS hostname/instance to id->hostname.
+ * Shares the process-wide ESP-IDF mDNS instance (init is idempotent).
+ * The identity is copied internally.
+ */
+esp_err_t dante_advertise_start(const dante_identity_t *id);
+
+/*
+ * Advertise a transmit channel as _netaudio-chan._udp. `index` is 0-based
+ * (published as the 1-based Dante channel id); `name` is the channel name
+ * (the DNS-SD instance becomes "<name>@<hostname>"). Call after
+ * dante_advertise_start().
+ */
+esp_err_t dante_advertise_tx_channel(uint8_t index, const char *name);
+
+/* Remove the Dante mDNS services advertised by this module. Does not touch
+ * services owned by other components (e.g. the AES67 _ravenna records). */
+void dante_advertise_stop(void);
+
+#ifdef __cplusplus
+}
+#endif

+ 60 - 0
components/dante/include/dante_device.h

@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Shared Dante device identity — clean-room, from docs/dante-protocol-notes.md.
+ *
+ * A Dante device is identified on the wire by a friendly hostname, an 8-byte
+ * factory device id (real hardware uses its MAC padded to 8 bytes), a process
+ * id (distinguishes multiple instances on one IP), and the set of UDP control
+ * ports it listens on. The advertiser and the control-protocol servers all
+ * work from one of these.
+ */
+#pragma once
+
+#include <stdint.h>
+#include "esp_err.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define DANTE_DEVICE_ID_LEN 8
+
+/* Standard Dante UDP control ports (as seen on hardware). */
+#define DANTE_PORT_ARC   4440   /* control / routing */
+#define DANTE_PORT_CMC   8800   /* device announce */
+#define DANTE_PORT_FLOWS 4455   /* flow setup (dbcp) */
+#define DANTE_PORT_INFO  8700   /* info / multicast */
+
+typedef struct {
+    char     hostname[64];                    /* friendly hostname: DNS-SD instance + SRV target (single DNS label, no spaces) */
+    uint8_t  device_id[DANTE_DEVICE_ID_LEN];  /* factory device id (MAC padded with zeros) */
+    uint16_t process_id;                      /* unique per instance on a given IP */
+    uint32_t ipv4;                            /* device IPv4, host byte order */
+    uint16_t arc_port;
+    uint16_t cmc_port;
+    uint16_t flows_port;
+    uint16_t info_port;
+    char     manufacturer[32];                /* mDNS "mf" */
+    char     model[32];                       /* mDNS "model" */
+    char     board_name[48];                  /* mDNS "router_info" */
+    uint32_t sample_rate;                     /* Hz */
+    uint8_t  bits_per_sample;                 /* 16 / 24 / 32 */
+} dante_identity_t;
+
+/*
+ * Populate an identity with sensible Dante defaults:
+ *   - standard control ports,
+ *   - device_id = the 6 MAC bytes followed by two zero bytes,
+ *   - a hostname derived from the last three MAC bytes,
+ *   - manufacturer/model/board placeholders, 48 kHz / 24-bit.
+ * The caller may override any field afterward (e.g. a nicer hostname).
+ * `mac` is the 6-byte Ethernet MAC; `ipv4` is host byte order.
+ */
+void dante_identity_init_default(dante_identity_t *id,
+                                 uint32_t ipv4, const uint8_t mac[6]);
+
+#ifdef __cplusplus
+}
+#endif

+ 160 - 0
components/dante/src/dante_advertise.c

@@ -0,0 +1,160 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Dante mDNS advertisement — clean-room, from docs/dante-protocol-notes.md.
+ */
+
+#include "dante_advertise.h"
+
+#include <stdio.h>
+#include <string.h>
+#include "esp_log.h"
+#include "mdns.h"
+
+static const char *TAG = "dante_adv";
+
+/* Protocol/version strings advertised in TXT records. These mimic values
+ * observed on real Dante hardware (they are ecosystem facts, not borrowed
+ * code) and are the most likely thing to need tuning against a given Dante
+ * Controller — keep them here so there's one place to adjust. */
+#define DANTE_ARCP_VERS   "2.7.41"
+#define DANTE_ARCP_MIN    "0.2.4"
+#define DANTE_ROUTER_VERS "4.0.2"
+#define DANTE_CMCP_VERS   "1.2.0"
+#define DANTE_CMCP_MIN    "1.0.0"
+#define DANTE_SERVER_VERS "4.0.2"
+/* CMC "channels" flags — exact meaning unknown; value observed on devices. */
+#define DANTE_CHANNELS_FLAGS "0x6000004d"
+
+#define DANTE_SVC_PROTO "_udp"
+
+static dante_identity_t s_id;
+static bool             s_started;
+
+/* Format the 8-byte factory device id as lowercase hex (no separators). */
+static void device_id_hex(const uint8_t *id, char *out, size_t out_len)
+{
+    static const char hexd[] = "0123456789abcdef";
+    size_t o = 0;
+    for (int i = 0; i < DANTE_DEVICE_ID_LEN && o + 2 < out_len; i++) {
+        out[o++] = hexd[(id[i] >> 4) & 0xF];
+        out[o++] = hexd[id[i] & 0xF];
+    }
+    out[o] = '\0';
+}
+
+esp_err_t dante_advertise_start(const dante_identity_t *id)
+{
+    if (id == NULL) {
+        return ESP_ERR_INVALID_ARG;
+    }
+    if (s_started) {
+        ESP_LOGW(TAG, "already advertising");
+        return ESP_ERR_INVALID_STATE;
+    }
+    s_id = *id;
+
+    esp_err_t err = mdns_init();
+    if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
+        ESP_LOGE(TAG, "mdns_init failed: %s", esp_err_to_name(err));
+        return err;
+    }
+
+    /* Dante requires a valid single-label hostname for the SRV target. This
+     * sets the process-wide mDNS hostname; if the AES67 stack also advertises,
+     * both must agree on this name (see header caveat). */
+    mdns_hostname_set(s_id.hostname);
+    mdns_instance_name_set(s_id.hostname);
+
+    char proc_str[8];
+    snprintf(proc_str, sizeof(proc_str), "%u", (unsigned)s_id.process_id);
+    char id_hex[DANTE_DEVICE_ID_LEN * 2 + 1];
+    device_id_hex(s_id.device_id, id_hex, sizeof(id_hex));
+
+    /* _netaudio-arc: the control endpoint. */
+    mdns_txt_item_t arc_txt[] = {
+        { "arcp_vers",   DANTE_ARCP_VERS },
+        { "arcp_min",    DANTE_ARCP_MIN },
+        { "router_vers", DANTE_ROUTER_VERS },
+        { "router_info", s_id.board_name },
+        { "mf",          s_id.manufacturer },
+        { "model",       s_id.model },
+    };
+    err = mdns_service_add(s_id.hostname, "_netaudio-arc", DANTE_SVC_PROTO,
+                           s_id.arc_port, arc_txt,
+                           sizeof(arc_txt) / sizeof(arc_txt[0]));
+    if (err != ESP_OK) {
+        ESP_LOGE(TAG, "add _netaudio-arc failed: %s", esp_err_to_name(err));
+        return err;
+    }
+
+    /* _netaudio-cmc: the device-announce endpoint. */
+    mdns_txt_item_t cmc_txt[] = {
+        { "id",          id_hex },
+        { "process",     proc_str },
+        { "cmcp_vers",   DANTE_CMCP_VERS },
+        { "cmcp_min",    DANTE_CMCP_MIN },
+        { "server_vers", DANTE_SERVER_VERS },
+        { "channels",    DANTE_CHANNELS_FLAGS },
+        { "mf",          s_id.manufacturer },
+        { "model",       s_id.model },
+    };
+    err = mdns_service_add(s_id.hostname, "_netaudio-cmc", DANTE_SVC_PROTO,
+                           s_id.cmc_port, cmc_txt,
+                           sizeof(cmc_txt) / sizeof(cmc_txt[0]));
+    if (err != ESP_OK) {
+        ESP_LOGE(TAG, "add _netaudio-cmc failed: %s", esp_err_to_name(err));
+        mdns_service_remove("_netaudio-arc", DANTE_SVC_PROTO);
+        return err;
+    }
+
+    s_started = true;
+    ESP_LOGI(TAG, "advertising as \"%s\" (id=%s, arc=%u cmc=%u)",
+             s_id.hostname, id_hex, s_id.arc_port, s_id.cmc_port);
+    return ESP_OK;
+}
+
+esp_err_t dante_advertise_tx_channel(uint8_t index, const char *name)
+{
+    if (!s_started || name == NULL) {
+        return ESP_ERR_INVALID_STATE;
+    }
+
+    char instance[96];
+    snprintf(instance, sizeof(instance), "%s@%s", name, s_id.hostname);
+
+    char id_str[8], rate_str[12], enc_str[8], pcm_str[16];
+    snprintf(id_str,   sizeof(id_str),   "%u", (unsigned)index + 1);   /* 1-based */
+    snprintf(rate_str, sizeof(rate_str), "%u", (unsigned)s_id.sample_rate);
+    snprintf(enc_str,  sizeof(enc_str),  "%u", (unsigned)s_id.bits_per_sample);
+    snprintf(pcm_str,  sizeof(pcm_str),  "%u", (unsigned)(s_id.bits_per_sample / 8));
+
+    mdns_txt_item_t chan_txt[] = {
+        { "txtvers", "2" },
+        { "id",      id_str },
+        { "rate",    rate_str },
+        { "enc",     enc_str },
+        { "pcm",     pcm_str },
+    };
+    esp_err_t err = mdns_service_add(instance, "_netaudio-chan", DANTE_SVC_PROTO,
+                                     s_id.flows_port, chan_txt,
+                                     sizeof(chan_txt) / sizeof(chan_txt[0]));
+    if (err != ESP_OK) {
+        ESP_LOGW(TAG, "add _netaudio-chan \"%s\" failed: %s",
+                 instance, esp_err_to_name(err));
+    }
+    return err;
+}
+
+void dante_advertise_stop(void)
+{
+    if (!s_started) {
+        return;
+    }
+    mdns_service_remove("_netaudio-chan", DANTE_SVC_PROTO);
+    mdns_service_remove("_netaudio-cmc",  DANTE_SVC_PROTO);
+    mdns_service_remove("_netaudio-arc",  DANTE_SVC_PROTO);
+    s_started = false;
+    ESP_LOGI(TAG, "stopped advertising");
+}

+ 45 - 0
components/dante/src/dante_device.c

@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Dante device identity helpers — clean-room.
+ */
+
+#include "dante_device.h"
+
+#include <stdio.h>
+#include <string.h>
+
+void dante_identity_init_default(dante_identity_t *id,
+                                 uint32_t ipv4, const uint8_t mac[6])
+{
+    if (id == NULL) {
+        return;
+    }
+    memset(id, 0, sizeof(*id));
+
+    /* factory device id = MAC (6 bytes) padded to 8 with zeros. */
+    if (mac != NULL) {
+        memcpy(id->device_id, mac, 6);
+    }
+    /* device_id[6], device_id[7] left zero by the memset above. */
+
+    /* hostname derived from the last three MAC bytes for uniqueness. */
+    snprintf(id->hostname, sizeof(id->hostname), "ESP32P4-Dante-%02X%02X%02X",
+             mac ? mac[3] : 0, mac ? mac[4] : 0, mac ? mac[5] : 0);
+
+    id->ipv4       = ipv4;
+    id->process_id = 0;   /* unique per instance; 0 is fine for a single device */
+
+    id->arc_port   = DANTE_PORT_ARC;
+    id->cmc_port   = DANTE_PORT_CMC;
+    id->flows_port = DANTE_PORT_FLOWS;
+    id->info_port  = DANTE_PORT_INFO;
+
+    strlcpy(id->manufacturer, "Bisonent",       sizeof(id->manufacturer));
+    strlcpy(id->model,        "ESP32-P4-ETH",   sizeof(id->model));
+    strlcpy(id->board_name,   "ESP32-P4 AoIP",  sizeof(id->board_name));
+
+    id->sample_rate     = 48000;
+    id->bits_per_sample = 24;
+}

+ 26 - 0
main/main.c

@@ -40,6 +40,8 @@
 #include "aes67_audio.h"
 #include "aes67_webui.h"
 #include "dante_discovery.h"
+#include "dante_advertise.h"
+#include "esp_mac.h"
 #include "dsps_tone_gen.h"
 #include "driver/i2c.h"
 #include "freertos/stream_buffer.h"
@@ -659,6 +661,30 @@ 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. */
+    {
+        static dante_identity_t s_dante_id;
+        uint8_t mac[6] = {0};
+        esp_read_mac(mac, ESP_MAC_ETH);
+
+        esp_netif_ip_info_t ip_info = {0};
+        esp_netif_t *dante_netif = esp_netif_get_handle_from_ifkey("ETH_0");
+        if (dante_netif) {
+            esp_netif_get_ip_info(dante_netif, &ip_info);
+        }
+        uint32_t ipv4 =
+            ((uint32_t)esp_ip4_addr1_16(&ip_info.ip) << 24) |
+            ((uint32_t)esp_ip4_addr2_16(&ip_info.ip) << 16) |
+            ((uint32_t)esp_ip4_addr3_16(&ip_info.ip) << 8)  |
+            ((uint32_t)esp_ip4_addr4_16(&ip_info.ip));
+
+        dante_identity_init_default(&s_dante_id, ipv4, mac);
+        dante_advertise_start(&s_dante_id);
+    }
+#endif
+
     /* Monitor loop: ISR health only */
     extern void aes67_audio_log_isr_stats(void);
     while (1) {