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 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 07:58:26 +10:00
parent ee03d9f9f1
commit 5f27262b27
7 changed files with 61 additions and 13 deletions
+18 -2
View File
@@ -1,7 +1,10 @@
#include "aes67_ptp.h" #include "aes67_ptp.h"
#include <string.h>
#include "aes67_cfg.h" #include "aes67_cfg.h"
#include "aes67_web.h" #include "aes67_web.h"
#include "esp_log.h"
#include "ptp_clock.h" #include "ptp_clock.h"
#include "ptp_hw.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 bool ptp_validate(const cJSON *g, char *err, size_t n)
{ {
static const char *const modes[] = { "multicast", "hybrid", NULL }; 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) && return cfg_check_enum(g, "mode", modes, err, n) &&
cfg_check_enum(g, "role", roles, err, n) && cfg_check_enum(g, "role", roles, err, n) &&
cfg_check_int(g, "domain", 0, 127, 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) 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) esp_err_t aes67_ptp_start(esp_eth_handle_t eth)
+3 -3
View File
@@ -622,8 +622,8 @@ static void load_config(void)
s.cfg_log_sync = cJSON_GetObjectItem(c, "log_sync")->valueint; s.cfg_log_sync = cJSON_GetObjectItem(c, "log_sync")->valueint;
s.cfg_log_announce = cJSON_GetObjectItem(c, "log_announce")->valueint; s.cfg_log_announce = cJSON_GetObjectItem(c, "log_announce")->valueint;
s.cfg_log_dreq = cJSON_GetObjectItem(c, "log_delay_req")->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 // Roles (docs): slave = clockClass 255, never transmits; auto = 248 with the configured priorities
// priorities (auto 250/250 by default, master e.g. p1 100). // (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.slave_only = strcmp(cJSON_GetObjectItem(c, "role")->valuestring, "slave") == 0;
s.hybrid = strcmp(cJSON_GetObjectItem(c, "mode")->valuestring, "hybrid") == 0; s.hybrid = strcmp(cJSON_GetObjectItem(c, "mode")->valuestring, "hybrid") == 0;
s.own_class = s.slave_only ? 255 : 248; s.own_class = s.slave_only ? 255 : 248;
@@ -711,7 +711,7 @@ static void ptp_task(void *arg)
int tos2 = s.dscp << 2; int tos2 = s.dscp << 2;
setsockopt(s.gen, IPPROTO_IP, IP_TOS, &tos2, sizeof(tos2)); 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", 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); s.own.p1, s.own.p2, s.domain);
// Re-run the decision: a foreign GM worse than our new dataset is dropped; as // Re-run the decision: a foreign GM worse than our new dataset is dropped; as
// TimeTransmitter with role slave we stop. // TimeTransmitter with role slave we stop.
+31
View File
@@ -295,6 +295,37 @@ esp_err_t cfg_set_number(const char *group, const char *key, double value, bool
return err; 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 ----- */ /* ----- HTTP ----- */
static esp_err_t config_get(httpd_req_t *req) static esp_err_t config_get(httpd_req_t *req)
+2
View File
@@ -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. // 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). // 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); 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. // 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); bool cfg_check_num(const cJSON *g, const char *key, double min, double max, char *err, size_t n);
+3 -3
View File
@@ -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. - 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. - 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). - 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. - 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). - PTP preset buttons: "Riedel SIC defaults" and "AES67 media profile defaults" fill the form (Save to apply).
## Config schema (core groups) ## 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} - 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) - 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) - 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: - Roles / BMCA dataset:
- slave (TimeReceiver only): clockClass 255, never transmits time. - 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. - 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). - clockAccuracy 0xFE (unknown), offsetScaledLogVariance 0xFFFF, timeSource 0xA0 (internal oscillator).
- clockIdentity = EUI-64 from MAC (xx-xx-xx-FF-FE-xx-xx-xx). - clockIdentity = EUI-64 from MAC (xx-xx-xx-FF-FE-xx-xx-xx).
- Note: Riedel Bolero reports its own clock class as 228. - Note: Riedel Bolero reports its own clock class as 228.
+1 -1
View File
@@ -123,7 +123,7 @@ The board locks its media clock to the PTP grandmaster on the network (IEEE 1588
| Setting | Default | Notes | | Setting | Default | Notes |
|---|---|---| |---|---|---|
| Mode | multicast | **hybrid** sends the delay requests unicast (fewer multicast packets). | | 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. | | Domain | 0 | Must match your grandmaster. |
| Priority 1 / 2 | 250 / 250 | Lower wins the grandmaster election. | | Priority 1 / 2 | 250 / 250 | Lower wins the grandmaster election. |
| Intervals, timeout, DSCP (46) | | Presets below set them. | | Intervals, timeout, DSCP (46) | | Presets below set them. |
+3 -4
View File
@@ -105,8 +105,7 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next
<small>hybrid: Delay_Req/Resp unicast</small></label> <small>hybrid: Delay_Req/Resp unicast</small></label>
<label><span>Role</span><select name="ptp.role"> <label><span>Role</span><select name="ptp.role">
<option value="slave">TimeReceiver only</option> <option value="slave">TimeReceiver only</option>
<option value="auto">Auto (TimeTransmitter if none)</option> <option value="auto">Auto (TimeTransmitter if none)</option></select></label>
<option value="master">Preferred TimeTransmitter</option></select></label>
<label><span>Domain</span><input name="ptp.domain" type="number" min="0" max="127"></label> <label><span>Domain</span><input name="ptp.domain" type="number" min="0" max="127"></label>
<label><span>Priority 1</span><input name="ptp.priority1" type="number" min="0" max="255"></label> <label><span>Priority 1</span><input name="ptp.priority1" type="number" min="0" max="255"></label>
<label><span>Priority 2</span><input name="ptp.priority2" type="number" min="0" max="255"></label> <label><span>Priority 2</span><input name="ptp.priority2" type="number" min="0" max="255"></label>
@@ -291,7 +290,7 @@ const CORE_DEF = {
log:{syslog:false,host:'',port:514,level:'info',facility:'16',format:'rfc5424'} 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). // 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. // PTP presets. riedel = Riedel Director defaults for a SIC AES67 card.
const PTP_PRESETS = { const PTP_PRESETS = {
riedel: {mode:'multicast', role:'slave', domain:0, priority1:128, priority2:126, 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; 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.`; 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 $ = id => document.getElementById(id);
const f = $('f'), sdpEl = $('sdp'), msg = $('msg'); const f = $('f'), sdpEl = $('sdp'), msg = $('msg');