From 5f27262b275a00994b0e23bced37f2c71459fa24 Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Sat, 26 Sep 2026 07:58:26 +1000 Subject: [PATCH] PTP: drop the "Preferred TimeTransmitter" role It behaved exactly like auto (clock class 248); the only difference was a priority1 suggestion in the page. Roles are now slave|auto; to prefer this device, use auto with a low priority1. A stored "master" is converted to auto at boot (new cfg_set_string, like cfg_set_number). Co-Authored-By: Claude Opus 5.5 --- components/aes67_ptp/aes67_ptp.c | 20 +++++++++++++-- components/aes67_ptp/ptp_clock.c | 6 ++--- components/aes67_web/cfg.c | 31 ++++++++++++++++++++++++ components/aes67_web/include/aes67_cfg.h | 2 ++ docs/aes67-core-base.md | 6 ++--- docs/user-guide.md | 2 +- web/index.html | 7 +++--- 7 files changed, 61 insertions(+), 13 deletions(-) diff --git a/components/aes67_ptp/aes67_ptp.c b/components/aes67_ptp/aes67_ptp.c index 9b6ae5c..08629a0 100644 --- a/components/aes67_ptp/aes67_ptp.c +++ b/components/aes67_ptp/aes67_ptp.c @@ -1,7 +1,10 @@ #include "aes67_ptp.h" +#include + #include "aes67_cfg.h" #include "aes67_web.h" +#include "esp_log.h" #include "ptp_clock.h" #include "ptp_hw.h" @@ -14,7 +17,7 @@ static const char PTP_DEFAULTS[] = static bool ptp_validate(const cJSON *g, char *err, size_t n) { static const char *const modes[] = { "multicast", "hybrid", NULL }; - static const char *const roles[] = { "slave", "auto", "master", NULL }; + static const char *const roles[] = { "slave", "auto", NULL }; return cfg_check_enum(g, "mode", modes, err, n) && cfg_check_enum(g, "role", roles, err, n) && cfg_check_int(g, "domain", 0, 127, err, n) && @@ -34,7 +37,20 @@ static void ptp_apply(const cJSON *g) esp_err_t aes67_ptp_init(void) { - return cfg_register("ptp", PTP_DEFAULTS, ptp_validate, ptp_apply); + esp_err_t err = cfg_register("ptp", PTP_DEFAULTS, ptp_validate, ptp_apply); + if (err != ESP_OK) { + return err; + } + // Role "master" was dropped (it behaved like auto; a low priority1 does the same). Stored values + // aren't re-validated, so convert it here. + cJSON *c = cfg_get("ptp"); + bool master = !strcmp(cJSON_GetObjectItemCaseSensitive(c, "role")->valuestring, "master"); + cJSON_Delete(c); + if (master) { + ESP_LOGW("ptp", "stored role \"master\" is no longer offered: now \"auto\" (same behaviour)"); + cfg_set_string("ptp", "role", "auto", false); + } + return ESP_OK; } esp_err_t aes67_ptp_start(esp_eth_handle_t eth) diff --git a/components/aes67_ptp/ptp_clock.c b/components/aes67_ptp/ptp_clock.c index 708a892..d82c8d4 100644 --- a/components/aes67_ptp/ptp_clock.c +++ b/components/aes67_ptp/ptp_clock.c @@ -622,8 +622,8 @@ static void load_config(void) s.cfg_log_sync = cJSON_GetObjectItem(c, "log_sync")->valueint; s.cfg_log_announce = cJSON_GetObjectItem(c, "log_announce")->valueint; s.cfg_log_dreq = cJSON_GetObjectItem(c, "log_delay_req")->valueint; - // Roles (docs): slave = clockClass 255, never transmits; auto/master = 248 with the configured - // priorities (auto 250/250 by default, master e.g. p1 100). + // Roles (docs): slave = clockClass 255, never transmits; auto = 248 with the configured priorities + // (250/250 by default: loses to any real GM; a lower priority1 prefers this device). s.slave_only = strcmp(cJSON_GetObjectItem(c, "role")->valuestring, "slave") == 0; s.hybrid = strcmp(cJSON_GetObjectItem(c, "mode")->valuestring, "hybrid") == 0; s.own_class = s.slave_only ? 255 : 248; @@ -711,7 +711,7 @@ static void ptp_task(void *arg) int tos2 = s.dscp << 2; setsockopt(s.gen, IPPROTO_IP, IP_TOS, &tos2, sizeof(tos2)); ESP_LOGI(TAG, "config applied: role %s, mode %s, p1 %u p2 %u, domain %u", - s.slave_only ? "slave" : "auto/master", s.hybrid ? "hybrid" : "multicast", + s.slave_only ? "slave" : "auto", s.hybrid ? "hybrid" : "multicast", s.own.p1, s.own.p2, s.domain); // Re-run the decision: a foreign GM worse than our new dataset is dropped; as // TimeTransmitter with role slave we stop. diff --git a/components/aes67_web/cfg.c b/components/aes67_web/cfg.c index 91c5887..28ce8b6 100644 --- a/components/aes67_web/cfg.c +++ b/components/aes67_web/cfg.c @@ -295,6 +295,37 @@ esp_err_t cfg_set_number(const char *group, const char *key, double value, bool return err; } +esp_err_t cfg_set_string(const char *group, const char *key, const char *value, bool apply) +{ + if (!s_lock) { + return ESP_ERR_INVALID_STATE; + } + xSemaphoreTake(s_lock, portMAX_DELAY); + cfg_group_t *g = find(group); + cJSON *cur = g ? cJSON_GetObjectItemCaseSensitive(g->values, key) : NULL; + esp_err_t err = ESP_OK; + if (!cur || !cJSON_IsString(cur)) { + err = ESP_ERR_NOT_FOUND; + } else { + cJSON_ReplaceItemInObjectCaseSensitive(g->values, key, cJSON_CreateString(value)); + nvs_handle_t h; + if (s_nvs_ok && (err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h)) == ESP_OK) { + err = store(h, g); + if (err == ESP_OK) { + err = nvs_commit(h); + } + nvs_close(h); + } + } + cJSON *copy = err == ESP_OK && apply && g->apply ? cJSON_Duplicate(g->values, true) : NULL; + xSemaphoreGive(s_lock); + if (copy) { + g->apply(copy); + cJSON_Delete(copy); + } + return err; +} + /* ----- HTTP ----- */ static esp_err_t config_get(httpd_req_t *req) diff --git a/components/aes67_web/include/aes67_cfg.h b/components/aes67_web/include/aes67_cfg.h index c32f392..0a27175 100644 --- a/components/aes67_web/include/aes67_cfg.h +++ b/components/aes67_web/include/aes67_cfg.h @@ -26,6 +26,8 @@ cJSON *cfg_get(const char *group); // Firmware-side change of one number (e.g. aes67.session_ver): stored like a POST, no validation. // apply = false skips the group's apply callback (e.g. to avoid restarting the stream). esp_err_t cfg_set_number(const char *group, const char *key, double value, bool apply); +// Same for a string value. +esp_err_t cfg_set_string(const char *group, const char *key, const char *value, bool apply); // Validation helpers for validate callbacks. Each returns false and fills err on failure. bool cfg_check_num(const cJSON *g, const char *key, double min, double max, char *err, size_t n); diff --git a/docs/aes67-core-base.md b/docs/aes67-core-base.md index 70150c3..83e8fd9 100644 --- a/docs/aes67-core-base.md +++ b/docs/aes67-core-base.md @@ -13,12 +13,12 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct - Status table values can be text or {cls: ok|warn|bad, text} for colour. - Device config is merged over defaults on load, so older firmware missing keys still renders. - Save bumps `aes67.session_ver` (SDP o= version). Save is blocked while any field is invalid (e.g. multicast out of range). -- UI uses IEEE 1588-2019 terms (TimeTransmitter / TimeReceiver), like Riedel. Config/API values stay master/slave/auto. +- UI uses IEEE 1588-2019 terms (TimeTransmitter / TimeReceiver), like Riedel. Config/API values stay slave/auto. - AES67 fields carry Riedel Director-style "Default / Range" hints. - PTP preset buttons: "Riedel SIC defaults" and "AES67 media profile defaults" fill the form (Save to apply). ## Config schema (core groups) -- ptp: {mode: multicast|hybrid, role: slave|auto|master, domain, priority1, priority2, log_sync, log_announce, announce_timeout, log_delay_req, dscp} (hardware timestamps always; not configurable) +- ptp: {mode: multicast|hybrid, role: slave|auto, domain, priority1, priority2, log_sync, log_announce, announce_timeout, log_delay_req, dscp} (hardware timestamps always; not configurable) - aes67: {name, enabled, discovery: manual|sap, mcast, port, ttl, dscp, channels, mono_sum, encoding: L16|L24, rate: 48000 only (all sources are 48 kHz), ptime, pt, ssrc, clk_offset, session_id, session_ver} - net: {hostname, vlan, web_on: both|inet|aes67, dhcp, ip, mask, gw, dns} (AES67 / untagged interface) - inet: {vlan_id, pcp, dhcp, ip, mask, gw, dns} (internet / tagged, only if net.vlan) @@ -52,7 +52,7 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct - Roles / BMCA dataset: - slave (TimeReceiver only): clockClass 255, never transmits time. - auto (default): clockClass 248, priority1 250, priority2 250. Loses to any real GM (Riedel default 128); takes over only if none. - - master (preferred TimeTransmitter): clockClass 248, priority1 100 (UI suggestion, editable). + - To prefer this device as TimeTransmitter, use auto with a lower priority1 (e.g. 100). (A separate "master" role was dropped: it behaved like auto; a stored "master" is converted to auto at boot.) - clockAccuracy 0xFE (unknown), offsetScaledLogVariance 0xFFFF, timeSource 0xA0 (internal oscillator). - clockIdentity = EUI-64 from MAC (xx-xx-xx-FF-FE-xx-xx-xx). - Note: Riedel Bolero reports its own clock class as 228. diff --git a/docs/user-guide.md b/docs/user-guide.md index 4a6bac5..196ff7b 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -123,7 +123,7 @@ The board locks its media clock to the PTP grandmaster on the network (IEEE 1588 | Setting | Default | Notes | |---|---|---| | Mode | multicast | **hybrid** sends the delay requests unicast (fewer multicast packets). | -| Role | Auto | **TimeReceiver only**: never becomes grandmaster. **Auto**: becomes grandmaster only if there is none (fallback). **Preferred TimeTransmitter**: tries to be grandmaster. | +| Role | Auto | **TimeReceiver only**: never becomes grandmaster. **Auto**: becomes grandmaster only if there is none (fallback). To make this board the preferred grandmaster, keep *Auto* and set a low *Priority 1* (e.g. 100). | | Domain | 0 | Must match your grandmaster. | | Priority 1 / 2 | 250 / 250 | Lower wins the grandmaster election. | | Intervals, timeout, DSCP (46) | | Presets below set them. | diff --git a/web/index.html b/web/index.html index 3b7d5e2..c2d8bc5 100644 --- a/web/index.html +++ b/web/index.html @@ -105,8 +105,7 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next hybrid: Delay_Req/Resp unicast + @@ -291,7 +290,7 @@ const CORE_DEF = { log:{syslog:false,host:'',port:514,level:'info',facility:'16',format:'rfc5424'} }; // Priority1 suggested when the role is changed in the UI (user can still edit it). -const ROLE_P1 = {auto:250, master:100}; +const ROLE_P1 = {auto:250}; // PTP presets. riedel = Riedel Director defaults for a SIC AES67 card. const PTP_PRESETS = { riedel: {mode:'multicast', role:'slave', domain:0, priority1:128, priority2:126, @@ -302,7 +301,7 @@ function ptpPreset(name) { for (const [k, v] of Object.entries(PTP_PRESETS[name])) f['ptp.' + k].value = v; toggle(); sdp(); msg.textContent = `Loaded ${name} PTP defaults – Save to apply.`; } -const CLOCK_CLASS = {slave:255, auto:248, master:248}; +const CLOCK_CLASS = {slave:255, auto:248}; const $ = id => document.getElementById(id); const f = $('f'), sdpEl = $('sdp'), msg = $('msg');