|
|
@@ -0,0 +1,204 @@
|
|
|
+/*
|
|
|
+ * SPDX-FileCopyrightText: 2026 Benjamin Harris
|
|
|
+ * SPDX-License-Identifier: Apache-2.0
|
|
|
+ *
|
|
|
+ * Dante mDNS discovery — clean-room, from docs/dante-protocol-notes.md.
|
|
|
+ */
|
|
|
+
|
|
|
+#include "dante_discovery.h"
|
|
|
+
|
|
|
+#include <stdio.h>
|
|
|
+#include <string.h>
|
|
|
+#include "freertos/FreeRTOS.h"
|
|
|
+#include "freertos/task.h"
|
|
|
+#include "esp_log.h"
|
|
|
+#include "mdns.h"
|
|
|
+#include "lwip/def.h"
|
|
|
+
|
|
|
+static const char *TAG = "dante_disc";
|
|
|
+
|
|
|
+/* DNS-SD service names for each Dante service type. All use "_udp".
|
|
|
+ * Order MUST match dante_service_type_t. */
|
|
|
+static const char *const s_service_names[DANTE_SVC_COUNT] = {
|
|
|
+ [DANTE_SVC_ARC] = "_netaudio-arc",
|
|
|
+ [DANTE_SVC_CMC] = "_netaudio-cmc",
|
|
|
+ [DANTE_SVC_CHAN] = "_netaudio-chan",
|
|
|
+ [DANTE_SVC_BUND] = "_netaudio-bund",
|
|
|
+};
|
|
|
+
|
|
|
+/* Cap on TXT records copied per instance (Dante channel records are well
|
|
|
+ * under this; the extra are ignored rather than risking a large stack copy). */
|
|
|
+#define DANTE_MAX_TXT 24
|
|
|
+
|
|
|
+/* Per-query result cap; a large Dante network can have many channels, but for
|
|
|
+ * discovery logging a bounded slice per scan is plenty. */
|
|
|
+#define DANTE_MAX_RESULTS 32
|
|
|
+
|
|
|
+/* Background scanner state. */
|
|
|
+static TaskHandle_t s_task;
|
|
|
+static volatile bool s_running;
|
|
|
+static uint32_t s_interval_ms;
|
|
|
+static uint32_t s_scan_timeout_ms;
|
|
|
+static dante_discovery_cb_t s_cb;
|
|
|
+static void *s_cb_user;
|
|
|
+
|
|
|
+const char *dante_service_type_str(dante_service_type_t svc)
|
|
|
+{
|
|
|
+ if (svc < 0 || svc >= DANTE_SVC_COUNT) {
|
|
|
+ return "?";
|
|
|
+ }
|
|
|
+ return s_service_names[svc];
|
|
|
+}
|
|
|
+
|
|
|
+/* Pull the first IPv4 address (host byte order) from an mDNS addr list. */
|
|
|
+static uint32_t first_ipv4(const mdns_ip_addr_t *addr)
|
|
|
+{
|
|
|
+ for (const mdns_ip_addr_t *a = addr; a != NULL; a = a->next) {
|
|
|
+ if (a->addr.type == ESP_IPADDR_TYPE_V4) {
|
|
|
+ return lwip_ntohl(a->addr.u_addr.ip4.addr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* Default reporter used when the caller passes cb == NULL: log the instance. */
|
|
|
+static void log_result(const dante_discovery_result_t *r, void *user)
|
|
|
+{
|
|
|
+ (void)user;
|
|
|
+ char ip[16] = "-";
|
|
|
+ if (r->ipv4) {
|
|
|
+ snprintf(ip, sizeof(ip), "%u.%u.%u.%u",
|
|
|
+ (unsigned)((r->ipv4 >> 24) & 0xFF),
|
|
|
+ (unsigned)((r->ipv4 >> 16) & 0xFF),
|
|
|
+ (unsigned)((r->ipv4 >> 8) & 0xFF),
|
|
|
+ (unsigned)(r->ipv4 & 0xFF));
|
|
|
+ }
|
|
|
+ ESP_LOGI(TAG, "%s %s (%s:%u)",
|
|
|
+ dante_service_type_str(r->service),
|
|
|
+ r->instance ? r->instance : "?",
|
|
|
+ ip, r->port);
|
|
|
+ for (size_t i = 0; i < r->txt_count; i++) {
|
|
|
+ ESP_LOGI(TAG, " %s=%s",
|
|
|
+ r->txt_keys[i] ? r->txt_keys[i] : "",
|
|
|
+ r->txt_vals[i] ? r->txt_vals[i] : "");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Walk one mDNS PTR result list, invoking cb for each instance. */
|
|
|
+static void report_results(dante_service_type_t svc,
|
|
|
+ mdns_result_t *results,
|
|
|
+ dante_discovery_cb_t cb, void *user)
|
|
|
+{
|
|
|
+ for (mdns_result_t *m = results; m != NULL; m = m->next) {
|
|
|
+ const char *txt_keys[DANTE_MAX_TXT];
|
|
|
+ const char *txt_vals[DANTE_MAX_TXT];
|
|
|
+ size_t n = m->txt_count < DANTE_MAX_TXT ? m->txt_count : DANTE_MAX_TXT;
|
|
|
+ for (size_t i = 0; i < n; i++) {
|
|
|
+ txt_keys[i] = m->txt[i].key;
|
|
|
+ txt_vals[i] = m->txt[i].value;
|
|
|
+ }
|
|
|
+
|
|
|
+ dante_discovery_result_t r = {
|
|
|
+ .service = svc,
|
|
|
+ .instance = m->instance_name,
|
|
|
+ .hostname = m->hostname,
|
|
|
+ .ipv4 = first_ipv4(m->addr),
|
|
|
+ .port = m->port,
|
|
|
+ .txt_keys = txt_keys,
|
|
|
+ .txt_vals = txt_vals,
|
|
|
+ .txt_count = n,
|
|
|
+ };
|
|
|
+ cb(&r, user);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+esp_err_t dante_discovery_scan(uint32_t timeout_ms,
|
|
|
+ dante_discovery_cb_t cb, void *user)
|
|
|
+{
|
|
|
+ /* mDNS is a shared, process-wide service. The AES67 stack initialises it
|
|
|
+ * during node start; if it hasn't yet, do it ourselves. Either way we
|
|
|
+ * never free it — we're only a querier. */
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cb == NULL) {
|
|
|
+ cb = log_result;
|
|
|
+ }
|
|
|
+
|
|
|
+ int total = 0;
|
|
|
+ for (int svc = 0; svc < DANTE_SVC_COUNT; svc++) {
|
|
|
+ mdns_result_t *results = NULL;
|
|
|
+ err = mdns_query_ptr(s_service_names[svc], "_udp",
|
|
|
+ timeout_ms, DANTE_MAX_RESULTS, &results);
|
|
|
+ if (err != ESP_OK) {
|
|
|
+ ESP_LOGW(TAG, "query %s failed: %s",
|
|
|
+ s_service_names[svc], esp_err_to_name(err));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (mdns_result_t *m = results; m != NULL; m = m->next) {
|
|
|
+ total++;
|
|
|
+ }
|
|
|
+ report_results((dante_service_type_t)svc, results, cb, user);
|
|
|
+ mdns_query_results_free(results);
|
|
|
+ }
|
|
|
+
|
|
|
+ ESP_LOGI(TAG, "scan complete: %d Dante service instance(s)", total);
|
|
|
+ return ESP_OK;
|
|
|
+}
|
|
|
+
|
|
|
+static void discovery_task(void *arg)
|
|
|
+{
|
|
|
+ (void)arg;
|
|
|
+ ESP_LOGI(TAG, "discovery task started (interval=%lums)",
|
|
|
+ (unsigned long)s_interval_ms);
|
|
|
+ while (s_running) {
|
|
|
+ dante_discovery_scan(s_scan_timeout_ms, s_cb, s_cb_user);
|
|
|
+ /* Sleep the interval in small slices so stop() is responsive. */
|
|
|
+ for (uint32_t waited = 0; s_running && waited < s_interval_ms;
|
|
|
+ waited += 200) {
|
|
|
+ vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ESP_LOGI(TAG, "discovery task stopped");
|
|
|
+ s_task = NULL;
|
|
|
+ vTaskDelete(NULL);
|
|
|
+}
|
|
|
+
|
|
|
+esp_err_t dante_discovery_start(uint32_t interval_ms,
|
|
|
+ dante_discovery_cb_t cb, void *user)
|
|
|
+{
|
|
|
+ if (s_task != NULL) {
|
|
|
+ ESP_LOGW(TAG, "discovery already running");
|
|
|
+ return ESP_ERR_INVALID_STATE;
|
|
|
+ }
|
|
|
+ /* Per-query wait: at most the interval, capped so a full sweep of all
|
|
|
+ * service types stays well under one interval. */
|
|
|
+ s_scan_timeout_ms = interval_ms / (2 * DANTE_SVC_COUNT);
|
|
|
+ if (s_scan_timeout_ms < 250) {
|
|
|
+ s_scan_timeout_ms = 250;
|
|
|
+ }
|
|
|
+ s_interval_ms = interval_ms;
|
|
|
+ s_cb = cb;
|
|
|
+ s_cb_user = user;
|
|
|
+ s_running = true;
|
|
|
+
|
|
|
+ /* Core 0 for cache coherency with the audio/net tasks; low priority —
|
|
|
+ * discovery is best-effort and must never contend with the audio path. */
|
|
|
+ BaseType_t ok = xTaskCreatePinnedToCore(discovery_task, "dante_disc",
|
|
|
+ 4096, NULL, 3, &s_task, 0);
|
|
|
+ if (ok != pdPASS) {
|
|
|
+ s_running = false;
|
|
|
+ s_task = NULL;
|
|
|
+ ESP_LOGE(TAG, "failed to create discovery task");
|
|
|
+ return ESP_ERR_NO_MEM;
|
|
|
+ }
|
|
|
+ return ESP_OK;
|
|
|
+}
|
|
|
+
|
|
|
+void dante_discovery_stop(void)
|
|
|
+{
|
|
|
+ s_running = false; /* task observes this and self-deletes */
|
|
|
+}
|