From 3d05768f00840fa5c199307373f395eb92de0943 Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Fri, 25 Sep 2026 06:38:55 +1000 Subject: [PATCH] Step 4.1: /stream.sdp from the live config and PTP GM - aes67_sdp_sap: aes67_sdp_build() builds the AES67 SDP (RFC 4566 + RFC 7273) with the same lines and order as the web UI preview; GET /stream.sdp serves it as application/sdp. - aes67_ptp: public aes67_ptp_gm_id(), aes67_ptp_locked(), aes67_ptp_now_ns(). Clock IDs are now uppercase (RFC 7273 style), also in status.ptp, which the UI copies into ts-refclk. - aes67_net: aes67_net_netif() returns the AES67 interface. - Verified on board: /stream.sdp byte-identical to the UI's sdp() for the same config/status; mono, L16, ptime 0.333, ttl 8, clk_offset 4294967295 and session_ver all follow the config. Co-Authored-By: Claude Opus 5.5 --- components/aes67_net/aes67_net.c | 5 ++ components/aes67_net/include/aes67_net.h | 4 + components/aes67_ptp/aes67_ptp.c | 20 +++++ components/aes67_ptp/include/aes67_ptp.h | 10 +++ components/aes67_ptp/ptp_clock.c | 18 +++- components/aes67_ptp/ptp_clock.h | 2 + components/aes67_sdp_sap/CMakeLists.txt | 3 +- components/aes67_sdp_sap/aes67_sdp_sap.c | 86 ++++++++++++++++++- .../aes67_sdp_sap/include/aes67_sdp_sap.h | 5 ++ main/CMakeLists.txt | 2 +- main/main.c | 2 + 11 files changed, 152 insertions(+), 5 deletions(-) diff --git a/components/aes67_net/aes67_net.c b/components/aes67_net/aes67_net.c index 819591f..f984828 100644 --- a/components/aes67_net/aes67_net.c +++ b/components/aes67_net/aes67_net.c @@ -183,3 +183,8 @@ esp_err_t aes67_net_init(esp_eth_handle_t eth) status_register(net_status); return esp_eth_start(eth); } + +esp_netif_t *aes67_net_netif(void) +{ + return s_netif; +} diff --git a/components/aes67_net/include/aes67_net.h b/components/aes67_net/include/aes67_net.h index 58cc097..d94dac5 100644 --- a/components/aes67_net/include/aes67_net.h +++ b/components/aes67_net/include/aes67_net.h @@ -4,6 +4,10 @@ #include "esp_err.h" #include "esp_eth_driver.h" +#include "esp_netif.h" // Attach a netif (DHCP client) to the Ethernet driver and start it. Logs link and IP events. esp_err_t aes67_net_init(esp_eth_handle_t eth); + +// The AES67 interface (untagged side once the VLAN split exists). NULL before aes67_net_init(). +esp_netif_t *aes67_net_netif(void); diff --git a/components/aes67_ptp/aes67_ptp.c b/components/aes67_ptp/aes67_ptp.c index 97e3132..76051e0 100644 --- a/components/aes67_ptp/aes67_ptp.c +++ b/components/aes67_ptp/aes67_ptp.c @@ -45,3 +45,23 @@ esp_err_t aes67_ptp_start(esp_eth_handle_t eth) } return err; } + +bool aes67_ptp_gm_id(char out[24]) +{ + return ptp_clock_gm_id(out); +} + +bool aes67_ptp_locked(void) +{ + return ptp_clock_locked(); +} + +esp_err_t aes67_ptp_now_ns(int64_t *ns) +{ + eth_mac_time_t t; + esp_err_t err = ptp_hw_get_time(&t); + if (err == ESP_OK) { + *ns = (int64_t)t.seconds * 1000000000LL + t.nanoseconds; + } + return err; +} diff --git a/components/aes67_ptp/include/aes67_ptp.h b/components/aes67_ptp/include/aes67_ptp.h index 9b6a89f..6a83327 100644 --- a/components/aes67_ptp/include/aes67_ptp.h +++ b/components/aes67_ptp/include/aes67_ptp.h @@ -2,6 +2,9 @@ // Core component: must not depend on main/ (project code). #pragma once +#include +#include + #include "esp_err.h" #include "esp_eth_driver.h" @@ -9,3 +12,10 @@ esp_err_t aes67_ptp_init(void); // Start the EMAC PTP clock and the PTP task on this Ethernet interface. esp_err_t aes67_ptp_start(esp_eth_handle_t eth); + +// Current GM clockIdentity as "XX-XX-XX-XX-XX-XX-XX-XX" (RFC 7273 form). false if none selected. +bool aes67_ptp_gm_id(char out[24]); +// Servo locked to the GM. +bool aes67_ptp_locked(void); +// PTP time (EMAC hardware clock) in ns. +esp_err_t aes67_ptp_now_ns(int64_t *ns); diff --git a/components/aes67_ptp/ptp_clock.c b/components/aes67_ptp/ptp_clock.c index ccd26ae..2b5548b 100644 --- a/components/aes67_ptp/ptp_clock.c +++ b/components/aes67_ptp/ptp_clock.c @@ -150,7 +150,7 @@ static int64_t mac_ns(const eth_mac_time_t *t) static void fmt_id(char *out, const uint8_t *id) { - sprintf(out, "%02x-%02x-%02x-%02x-%02x-%02x-%02x-%02x", id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]); + sprintf(out, "%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X", id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]); } // IEEE 1588 dataset comparison (without the topology part): <0 if a is better. @@ -487,6 +487,22 @@ static void ptp_task(void *arg) } } +bool ptp_clock_gm_id(char out[24]) +{ + xSemaphoreTake(s.lock, portMAX_DELAY); + bool valid = s.gm.valid; + if (valid) { + fmt_id(out, s.gm.gm_id); + } + xSemaphoreGive(s.lock); + return valid; +} + +bool ptp_clock_locked(void) +{ + return s.locked; +} + void ptp_clock_status(cJSON *st) { cJSON *p = cJSON_AddObjectToObject(st, "ptp"); diff --git a/components/aes67_ptp/ptp_clock.h b/components/aes67_ptp/ptp_clock.h index 8628aa4..6bcf2a0 100644 --- a/components/aes67_ptp/ptp_clock.h +++ b/components/aes67_ptp/ptp_clock.h @@ -6,5 +6,7 @@ #include "esp_netif.h" esp_err_t ptp_clock_start(esp_netif_t *netif); +bool ptp_clock_gm_id(char out[24]); +bool ptp_clock_locked(void); // Adds the "ptp" object to /api/status. void ptp_clock_status(cJSON *st); diff --git a/components/aes67_sdp_sap/CMakeLists.txt b/components/aes67_sdp_sap/CMakeLists.txt index 39a7a70..fbb348e 100644 --- a/components/aes67_sdp_sap/CMakeLists.txt +++ b/components/aes67_sdp_sap/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRCS "aes67_sdp_sap.c" - INCLUDE_DIRS "include") + INCLUDE_DIRS "include" + PRIV_REQUIRES aes67_net aes67_ptp aes67_web esp_netif) diff --git a/components/aes67_sdp_sap/aes67_sdp_sap.c b/components/aes67_sdp_sap/aes67_sdp_sap.c index bb5ebdd..553526d 100644 --- a/components/aes67_sdp_sap/aes67_sdp_sap.c +++ b/components/aes67_sdp_sap/aes67_sdp_sap.c @@ -1,7 +1,89 @@ #include "aes67_sdp_sap.h" -// Stub (build step 0). +#include +#include + +#include "aes67_cfg.h" +#include "aes67_net.h" +#include "aes67_ptp.h" +#include "aes67_web.h" +#include "esp_netif.h" + +#define SDP_MAX 1024 + +// Same lines, in the same order, as the SDP preview in web/index.html. +size_t aes67_sdp_build(char *buf, size_t size) +{ + cJSON *a = cfg_get("aes67"); + cJSON *p = cfg_get("ptp"); + if (!a || !p) { + cJSON_Delete(a); + cJSON_Delete(p); + return 0; + } +#define NUM(o, k) cJSON_GetObjectItemCaseSensitive(o, k)->valuedouble +#define STR(o, k) cJSON_GetObjectItemCaseSensitive(o, k)->valuestring + char ip[16] = "0.0.0.0"; + esp_netif_ip_info_t info; + esp_netif_t *netif = aes67_net_netif(); + if (netif && esp_netif_get_ip_info(netif, &info) == ESP_OK && info.ip.addr) { + snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip)); + } + char gm[24]; + if (!aes67_ptp_gm_id(gm)) { + strcpy(gm, "00-00-00-00-00-00-00-00"); + } + int ch = (int)NUM(a, "channels"); + char info_ch[16]; + snprintf(info_ch, sizeof(info_ch), ch == 1 ? "mono" : ch == 2 ? "stereo" : "%d ch", ch); + int dom = (int)NUM(p, "domain"); + int pt = (int)NUM(a, "pt"); + + int n = snprintf(buf, size, + "v=0\r\n" + "o=- %.0f %.0f IN IP4 %s\r\n" + "s=%s\r\n" + "c=IN IP4 %s/%d\r\n" + "t=0 0\r\n" + "a=clock-domain:PTPv2 %d\r\n" + "m=audio %d RTP/AVP %d\r\n" + "i=%s\r\n" + "a=rtpmap:%d %s/%d/%d\r\n" + "a=recvonly\r\n" + "a=ptime:%g\r\n" + "a=ts-refclk:ptp=IEEE1588-2008:%s:%d\r\n" + "a=mediaclk:direct=%.0f\r\n", + NUM(a, "session_id"), NUM(a, "session_ver"), ip, + STR(a, "name"), + STR(a, "mcast"), (int)NUM(a, "ttl"), + dom, + (int)NUM(a, "port"), pt, + info_ch, + pt, STR(a, "encoding"), (int)NUM(a, "rate"), ch, + NUM(a, "ptime"), + gm, dom, + NUM(a, "clk_offset")); +#undef NUM +#undef STR + cJSON_Delete(a); + cJSON_Delete(p); + return n > 0 && (size_t)n < size ? (size_t)n : 0; +} + +static esp_err_t sdp_get(httpd_req_t *req) +{ + char buf[SDP_MAX]; + size_t len = aes67_sdp_build(buf, sizeof(buf)); + if (!len) { + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "SDP build failed"); + } + httpd_resp_set_type(req, "application/sdp"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + return httpd_resp_send(req, buf, len); +} + esp_err_t aes67_sdp_sap_init(void) { - return ESP_OK; + static const httpd_uri_t uri = { .uri = "/stream.sdp", .method = HTTP_GET, .handler = sdp_get }; + return web_register_uri(&uri); } diff --git a/components/aes67_sdp_sap/include/aes67_sdp_sap.h b/components/aes67_sdp_sap/include/aes67_sdp_sap.h index f1e4939..110ab96 100644 --- a/components/aes67_sdp_sap/include/aes67_sdp_sap.h +++ b/components/aes67_sdp_sap/include/aes67_sdp_sap.h @@ -2,6 +2,11 @@ // Core component: must not depend on main/ (project code). #pragma once +#include + #include "esp_err.h" +// Registers GET /stream.sdp. (SAP: step 5.) esp_err_t aes67_sdp_sap_init(void); +// Build the current SDP (RFC 4566 + RFC 7273, AES67). Returns the length, or 0 if it didn't fit. +size_t aes67_sdp_build(char *buf, size_t size); diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e8612d6..501eb91 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register(SRCS "main.c" "project_cfg.c" INCLUDE_DIRS "." REQUIRES esp_app_format esp_hw_support - aes67_board aes67_health aes67_net aes67_ota aes67_ptp + aes67_board aes67_health aes67_net aes67_ota aes67_ptp aes67_sdp_sap aes67_syslog aes67_tx aes67_web) diff --git a/main/main.c b/main/main.c index d692bc5..4a26dec 100644 --- a/main/main.c +++ b/main/main.c @@ -3,6 +3,7 @@ #include "aes67_net.h" #include "aes67_ota.h" #include "aes67_ptp.h" +#include "aes67_sdp_sap.h" #include "aes67_syslog.h" #include "aes67_tx.h" #include "aes67_web.h" @@ -36,5 +37,6 @@ void app_main(void) ESP_ERROR_CHECK(aes67_syslog_init()); project_cfg_register(); ESP_ERROR_CHECK(aes67_ota_init()); + ESP_ERROR_CHECK(aes67_sdp_sap_init()); ESP_ERROR_CHECK(aes67_web_start()); }