|
|
@@ -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");
|
|
|
+}
|