Compare commits

..

5 Commits

Author SHA1 Message Date
bsncubed e763dc81bd Phase 2: NTP with hostnames (pool.ntp.org) or IPs
Planned SNTP client: servers as names or IPs, several allowed, resolved
via DNS. Seeds the PTP clock with real time before becoming GM (today it
starts at 1970) and gives syslog timestamps. Added to the phase 2 list
in CLAUDE.md and as a section in aes67-core-base.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:22:58 +10:00
bsncubed 5bd1a20e43 Step 6.3: PTP hybrid mode (unicast Delay_Req/Resp), step 6 done
- As TimeReceiver in ptp.mode hybrid: Delay_Req unicast to the GM (IP
  from its Announce, MAC recorded by the RX hook from its Sync frames),
  unicastFlag set.
- As TimeTransmitter: a Delay_Req with the unicastFlag gets a unicast
  Delay_Resp; multicast requests get multicast replies.
- EMAC timestamps every received frame (en_ts4all): its PTP filter left
  unicast Delay_Req unstamped ("no HW RX timestamp").
- A unicast Delay_Resp's logMessageInterval 0x7F is ignored (use
  ptp.log_delay_req); log_us() clamps to -7..6 (the shift was undefined).
- Verified vs ptp4l --hybrid_e2e 1: board hybrid TimeReceiver 18
  Delay_Req in 20 s, all answered, locked; board TimeTransmitter
  (p1 100): tcpdump shows Sync/Follow_Up/Announce to 224.0.1.129,
  Delay_Req 192.168.192.233 -> .244 [unicast] and Delay_Resp .244 ->
  .233 [unicast] within 0.4 ms; ptp4l s2.
- Docs: TimeTransmitter/hybrid notes; step 6 ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:21:09 +10:00
bsncubed dcea39b216 Step 6.2: role hand-over, session_ver bump on GM change
- aes67_cfg: cfg_set_number(group, key, value, apply) for firmware-side
  changes, stored in NVS like a POST; apply can be skipped.
- aes67_sdp_sap: once a second, a change of the PTP GM (ts-refclk) bumps
  aes67.session_ver without re-applying the aes67 group (no stream
  restart); SAP then re-announces. Runs whether or not SAP is on.
- Verified with ptp4l as a normal clock (p1 128): board auto (p1 250)
  steps down to SLAVE (session_ver 1->2); role master p1 100 via API ->
  board MASTER at once (->3); back to auto p1 250 -> ptp4l takes over
  within ~1 s, board SLAVE again (->4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:08:00 +10:00
bsncubed 08b83a069c Step 6.1: PTP TimeTransmitter with BMCA (multicast)
- Own dataset from config: slave -> class 255, never TimeTransmitter;
  auto/master -> class 248 with ptp.priority1/2; accuracy 0xFE, variance
  0xFFFF, timeSource 0xA0, clockIdentity EUI-64 from MAC.
- BMCA: a foreign TimeTransmitter is followed only if its Announce beats
  our dataset (always for slave-only); if the followed one turns worse we
  take over; a better one makes us step down. LISTENING -> MASTER after
  announceReceiptTimeout x our announce interval.
- MASTER: Announce (PTP timescale flag) every 2^log_announce, two-step
  Sync every 2^log_sync (raw frame, HW TX time in Follow_Up), Delay_Resp
  for each Delay_Req with its HW RX time and logMessageInterval =
  log_delay_req. Frequency correction kept (holdover).
- ptp config applies live (role, priorities, intervals, domain, DSCP).
- status.ptp: state MASTER, gm_id = own, TX Sync/Announce intervals,
  Delay_Req/Resp counters; locked is true as master so AES67 TX keeps
  running; SDP ts-refclk and SAP follow.
- Verified: with ptp4l GM p1 128 the board (auto, p1 250) stays SLAVE;
  ptp4l stopped -> board MASTER; ptp4l -s --priority1 255 locks to it
  (s2) in ~4 s, offset within +-320 ns, path delay 10.3 us.
  Known: Sync send times jitter ~10 ms (FreeRTOS tick); accuracy is not
  affected (two-step Follow_Up carries the exact HW time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:42:43 +10:00
bsncubed 6d85eb49f8 lwIP: 16 sockets so web clients are not starved
With the default 10, PTP (2), AES67 TX, SAP, syslog and httpd's
listen/control sockets left ~3 for web clients although httpd allows 7.
A browser's keep-alive connections then made new requests fail with a
reset for seconds at a time (the intermittent 'outages').
Reproduced: with 1 idle connection held, new requests were reset.
After: 6 idle connections held, new requests answered in 6-7 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:35:02 +10:00
11 changed files with 422 additions and 24 deletions
+2 -1
View File
@@ -49,9 +49,10 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4
- [ ] 4. AES67 TX with a 1 kHz test tone, PTP-paced; /stream.sdp. Verify: import SDP on a Riedel Artist 4-wire AES67 port, and check packets/timestamps in Wireshark. - [ ] 4. AES67 TX with a 1 kHz test tone, PTP-paced; /stream.sdp. Verify: import SDP on a Riedel Artist 4-wire AES67 port, and check packets/timestamps in Wireshark.
Done and checked with a receiver script (all ptimes, L16/L24, tone phase-locked to PTP); still open: the Riedel import and a Wireshark capture. Done and checked with a receiver script (all ptimes, L16/L24, tone phase-locked to PTP); still open: the Riedel import and a Wireshark capture.
- [x] 5. SAP discovery, then syslog, then health/temperatures (one at a time). VLAN split moved to phase 2. - [x] 5. SAP discovery, then syslog, then health/temperatures (one at a time). VLAN split moved to phase 2.
- [ ] 6. PTP TimeTransmitter: BMCA roles (auto/master), hybrid mode. - [x] 6. PTP TimeTransmitter: BMCA roles (auto/master), hybrid mode.
- [ ] 7. Sources: HLS player, then cspot (Spotify Connect), then failover + /api/player. - [ ] 7. Sources: HLS player, then cspot (Spotify Connect), then failover + /api/player.
- [ ] 8. Mono sum, gain, polish. - [ ] 8. Mono sum, gain, polish.
## Phase 2 (parked) ## Phase 2 (parked)
- [ ] VLAN split: AES67 untagged + internet tagged (inet.vlan_id/pcp), per aes67-core-base.md "Network". Needs a tagged VLAN with DHCP on the switch port. Config group `inet` and the UI fields exist already; nothing is applied yet. - [ ] VLAN split: AES67 untagged + internet tagged (inet.vlan_id/pcp), per aes67-core-base.md "Network". Needs a tagged VLAN with DHCP on the switch port. Config group `inet` and the UI fields exist already; nothing is applied yet.
- [ ] NTP (SNTP): servers as hostnames or IPs (e.g. `pool.ntp.org`, several allowed; names resolved via DNS, re-resolved on failure). Uses: seed the PTP clock with real time before becoming GM (today it starts at 1970), syslog timestamps. Needs a `time` config group + UI fields (change doc and page together).
+6 -1
View File
@@ -27,9 +27,14 @@ static bool ptp_validate(const cJSON *g, char *err, size_t n)
cfg_check_int(g, "dscp", 0, 63, err, n); cfg_check_int(g, "dscp", 0, 63, err, n);
} }
static void ptp_apply(const cJSON *g)
{
ptp_clock_reconfig();
}
esp_err_t aes67_ptp_init(void) esp_err_t aes67_ptp_init(void)
{ {
return cfg_register("ptp", PTP_DEFAULTS, ptp_validate, NULL); return cfg_register("ptp", PTP_DEFAULTS, ptp_validate, ptp_apply);
} }
esp_err_t aes67_ptp_start(esp_eth_handle_t eth) esp_err_t aes67_ptp_start(esp_eth_handle_t eth)
+322 -20
View File
@@ -26,6 +26,10 @@
#define MAX_DRIFT_PPB 500000.0 #define MAX_DRIFT_PPB 500000.0
#define WINDOW 64 // samples for interval/delay statistics #define WINDOW 64 // samples for interval/delay statistics
#define SUMMARY_US (60 * 1000000LL) #define SUMMARY_US (60 * 1000000LL)
#define FLAG_PTP_TIMESCALE 0x0008 // flagField octet 1 bit 3
#define FLAG_UNICAST 0x0400 // flagField octet 0 bit 2
#define UTC_OFFSET 37 // TAI - UTC (s), announced as information only
#define TIME_SOURCE_OSC 0xA0 // internal oscillator
static const char *TAG = "ptp"; static const char *TAG = "ptp";
static const uint8_t PTP_MCAST_MAC[6] = { 0x01, 0x00, 0x5e, 0x00, 0x01, 0x81 }; static const uint8_t PTP_MCAST_MAC[6] = { 0x01, 0x00, 0x5e, 0x00, 0x01, 0x81 };
@@ -54,7 +58,20 @@ static struct {
int ev, gen; int ev, gen;
uint8_t domain, dscp, timeout; uint8_t domain, dscp, timeout;
uint8_t port_id[10]; // our clockId (EUI-64 from MAC) + port 1 uint8_t port_id[10]; // our clockId (EUI-64 from MAC) + port 1
master_t gm; master_t gm; // foreign TimeTransmitter we follow (valid = TimeReceiver)
master_t own; // our own dataset for BMCA
bool slave_only; // role "slave": never TimeTransmitter
bool hybrid; // mode "hybrid": Delay_Req/Resp unicast as TimeReceiver
uint8_t gm_mac[6]; // Ethernet source of the GM's Sync (for unicast Delay_Req)
bool have_gm_mac;
uint32_t dreq_logged_ip; // last Delay_Req destination logged
bool master; // we are the TimeTransmitter
int64_t listen_since_us; // start of LISTENING (for the announce receipt timeout)
volatile bool reconfig;
int8_t cfg_log_sync, cfg_log_announce, cfg_log_dreq;
// TimeTransmitter
uint16_t tx_sync_seq, tx_announce_seq;
int64_t next_sync_us, next_announce_us, prev_t1_tx;
// Sync / Follow_Up // Sync / Follow_Up
uint16_t sync_seq; uint16_t sync_seq;
bool sync_pending; bool sync_pending;
@@ -80,7 +97,7 @@ static struct {
bool locked; bool locked;
// Statistics for status.ptp (guarded by lock) // Statistics for status.ptp (guarded by lock)
SemaphoreHandle_t lock; SemaphoreHandle_t lock;
window_t sync_iv, announce_iv, delays; window_t sync_iv, announce_iv, delays; // RX intervals as receiver, TX intervals as transmitter
int64_t last_announce_us; int64_t last_announce_us;
uint32_t delay_req, delay_resp; uint32_t delay_req, delay_resp;
uint8_t own_class; uint8_t own_class;
@@ -122,9 +139,17 @@ static void win_stats(const window_t *w, double *mean, double *min, double *max,
#define LOCKED(stmt) do { xSemaphoreTake(s.lock, portMAX_DELAY); stmt; xSemaphoreGive(s.lock); } while (0) #define LOCKED(stmt) do { xSemaphoreTake(s.lock, portMAX_DELAY); stmt; xSemaphoreGive(s.lock); } while (0)
static void set_locked(bool locked); static void set_locked(bool locked);
static void become_master(void);
/* ----- helpers ----- */ /* ----- helpers ----- */
// 2^log seconds in us; log clamped to the sane PTP range (-7..6).
static int64_t log_us(int8_t log)
{
log = log < -7 ? -7 : log > 6 ? 6 : log;
return log >= 0 ? 1000000LL << log : 1000000LL >> -log;
}
static uint16_t rd16(const uint8_t *p) { return (p[0] << 8) | p[1]; } static uint16_t rd16(const uint8_t *p) { return (p[0] << 8) | p[1]; }
static int64_t rd_ts(const uint8_t *p) // 48-bit seconds + 32-bit ns static int64_t rd_ts(const uint8_t *p) // 48-bit seconds + 32-bit ns
@@ -205,6 +230,19 @@ static void on_announce(const uint8_t *b, int len, uint32_t src_ip)
if (memcmp(m.port_id, s.port_id, 10) == 0) { if (memcmp(m.port_id, s.port_id, 10) == 0) {
return; // our own return; // our own
} }
// BMCA: follow a foreign TimeTransmitter only if it beats our own dataset (always when slave-only).
bool better = s.slave_only || compare(&m, &s.own) < 0;
if (!better) {
if (from_gm(b)) {
ESP_LOGI(TAG, "TimeTransmitter's dataset is now worse than ours");
become_master();
}
return; // as TimeTransmitter a worse one should yield; nothing to do
}
if (s.master) {
s.master = false;
ESP_LOGI(TAG, "better TimeTransmitter seen: leaving TimeTransmitter state");
}
if (from_gm(b)) { if (from_gm(b)) {
LOCKED({ LOCKED({
if (s.last_announce_us) { if (s.last_announce_us) {
@@ -226,6 +264,7 @@ static void on_announce(const uint8_t *b, int len, uint32_t src_ip)
win_clear(&s.delays); win_clear(&s.delays);
}); });
s.sync_pending = s.dreq_pending = false; s.sync_pending = s.dreq_pending = false;
s.have_gm_mac = false;
LOCKED({ LOCKED({
s.delay_ns = s.prev_t2 = 0; s.delay_ns = s.prev_t2 = 0;
s.stepped = false; s.stepped = false;
@@ -335,10 +374,11 @@ static void on_sync(const uint8_t *b, int len)
} }
eth_mac_time_t t2; eth_mac_time_t t2;
uint16_t seq = rd16(b + 30); uint16_t seq = rd16(b + 30);
if (!ptp_hw_rx_ts(PTP_MSG_SYNC, seq, b + 20, &t2)) { if (!ptp_hw_rx_ts_mac(PTP_MSG_SYNC, seq, b + 20, &t2, s.gm_mac)) {
ESP_LOGW(TAG, "Sync %u: no HW RX timestamp", seq); ESP_LOGW(TAG, "Sync %u: no HW RX timestamp", seq);
return; return;
} }
s.have_gm_mac = true;
s.sync_seq = seq; s.sync_seq = seq;
s.log_sync = (int8_t)b[33]; s.log_sync = (int8_t)b[33];
s.t2 = mac_ns(&t2); s.t2 = mac_ns(&t2);
@@ -375,9 +415,22 @@ static void send_delay_req(void)
m[32] = 1; // controlField: Delay_Req m[32] = 1; // controlField: Delay_Req
m[33] = 0x7f; m[33] = 0x7f;
uint32_t dst; uint32_t dst;
const uint8_t *dst_mac = PTP_MCAST_MAC;
inet_aton(PTP_MCAST, (struct in_addr *)&dst); inet_aton(PTP_MCAST, (struct in_addr *)&dst);
if (s.hybrid && s.gm.ip && s.have_gm_mac) {
// Hybrid: unicast to the GM (address from its Announce, MAC from its Sync frames).
dst = s.gm.ip;
dst_mac = s.gm_mac;
m[6] |= FLAG_UNICAST >> 8;
}
if (dst != s.dreq_logged_ip) {
s.dreq_logged_ip = dst;
ESP_LOGI(TAG, "Delay_Req %s to " IPSTR " (%02x:%02x:%02x:%02x:%02x:%02x)",
dst_mac == PTP_MCAST_MAC ? "multicast" : "unicast", IP2STR((esp_ip4_addr_t *)&dst),
dst_mac[0], dst_mac[1], dst_mac[2], dst_mac[3], dst_mac[4], dst_mac[5]);
}
eth_mac_time_t t3; eth_mac_time_t t3;
esp_err_t err = ptp_hw_send_event(PTP_MCAST_MAC, dst, s.dscp, m, sizeof(m), &t3); esp_err_t err = ptp_hw_send_event(dst_mac, dst, s.dscp, m, sizeof(m), &t3);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Delay_Req %u: %s", s.dreq_seq, esp_err_to_name(err)); ESP_LOGW(TAG, "Delay_Req %u: %s", s.dreq_seq, esp_err_to_name(err));
s.dreq_pending = false; s.dreq_pending = false;
@@ -397,7 +450,10 @@ static void on_delay_resp(const uint8_t *b, int len)
s.dreq_pending = false; s.dreq_pending = false;
LOCKED(s.delay_resp++); LOCKED(s.delay_resp++);
int64_t t4 = rd_ts(b + 34) - rd_corr_ns(b + 8); int64_t t4 = rd_ts(b + 34) - rd_corr_ns(b + 8);
s.log_dreq = (int8_t)b[33]; // Delay_Req interval from the GM; a unicast Delay_Resp carries 0x7F ("not specified"):
// then use our configured interval.
int8_t l = (int8_t)b[33];
s.log_dreq = l >= -7 && l <= 6 ? l : s.cfg_log_dreq;
if (!s.prev_t2) { if (!s.prev_t2) {
return; return;
} }
@@ -409,6 +465,152 @@ static void on_delay_resp(const uint8_t *b, int len)
}); });
} }
/* ----- TimeTransmitter ----- */
static void wr_ts(uint8_t *p, int64_t ns)
{
uint64_t sec = ns / 1000000000LL;
uint32_t n = ns % 1000000000LL;
p[0] = sec >> 40; p[1] = sec >> 32; p[2] = sec >> 24; p[3] = sec >> 16; p[4] = sec >> 8; p[5] = sec;
p[6] = n >> 24; p[7] = n >> 16; p[8] = n >> 8; p[9] = n;
}
static void hdr(uint8_t *m, uint8_t type, uint16_t len, uint16_t flags, uint16_t seq, uint8_t control, int8_t log)
{
memset(m, 0, len);
m[0] = type;
m[1] = 2;
m[2] = len >> 8;
m[3] = len & 0xff;
m[4] = s.domain;
m[6] = flags >> 8;
m[7] = flags & 0xff;
memcpy(m + 20, s.port_id, 10);
m[30] = seq >> 8;
m[31] = seq & 0xff;
m[32] = control;
m[33] = (uint8_t)log;
}
// General messages (Announce, Follow_Up, Delay_Resp) go through the lwIP socket on port 320.
static void send_general(const uint8_t *m, size_t len, uint32_t dst_ip)
{
struct sockaddr_in dst = { .sin_family = AF_INET, .sin_port = htons(PTP_GENERAL_PORT), .sin_addr.s_addr = dst_ip };
if (sendto(s.gen, m, len, 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
ESP_LOGW(TAG, "send type %u failed (errno %d)", m[0] & 0x0f, errno);
}
}
static uint32_t mcast_ip(void)
{
struct in_addr a;
inet_aton(PTP_MCAST, &a);
return a.s_addr;
}
static void send_announce(void)
{
uint8_t m[64];
hdr(m, PTP_MSG_ANNOUNCE, sizeof(m), FLAG_PTP_TIMESCALE, s.tx_announce_seq++, 5, s.cfg_log_announce);
m[44] = UTC_OFFSET >> 8;
m[45] = UTC_OFFSET & 0xff;
m[47] = s.own.p1;
m[48] = s.own.cls;
m[49] = s.own.acc;
m[50] = s.own.var >> 8;
m[51] = s.own.var & 0xff;
m[52] = s.own.p2;
memcpy(m + 53, s.own.gm_id, 8);
m[63] = TIME_SOURCE_OSC;
send_general(m, sizeof(m), mcast_ip());
int64_t now = esp_timer_get_time();
LOCKED({
if (s.last_announce_us) {
win_add(&s.announce_iv, now - s.last_announce_us);
}
s.last_announce_us = now;
});
}
// Two-step: Sync as a raw frame for its hardware TX timestamp, then Follow_Up with that time.
static void send_sync(void)
{
uint8_t m[44];
uint16_t seq = s.tx_sync_seq++;
hdr(m, PTP_MSG_SYNC, sizeof(m), FLAG_TWO_STEP, seq, 0, s.cfg_log_sync);
eth_mac_time_t t1;
esp_err_t err = ptp_hw_send_event(PTP_MCAST_MAC, mcast_ip(), s.dscp, m, sizeof(m), &t1);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Sync %u: %s", seq, esp_err_to_name(err));
return;
}
hdr(m, PTP_MSG_FOLLOW_UP, sizeof(m), 0, seq, 2, s.cfg_log_sync);
int64_t t1n = mac_ns(&t1);
wr_ts(m + 34, t1n);
send_general(m, sizeof(m), mcast_ip());
LOCKED({
if (s.prev_t1_tx) {
win_add(&s.sync_iv, t1n - s.prev_t1_tx);
}
s.prev_t1_tx = t1n;
});
}
static void on_delay_req(const uint8_t *b, int len, uint32_t src_ip)
{
if (!s.master || len < 44) {
return;
}
eth_mac_time_t t4;
uint16_t seq = rd16(b + 30);
if (!ptp_hw_rx_ts(PTP_MSG_DELAY_REQ, seq, b + 20, &t4)) {
ESP_LOGW(TAG, "Delay_Req %u: no HW RX timestamp", seq);
return;
}
uint8_t m[54];
hdr(m, PTP_MSG_DELAY_RESP, sizeof(m), 0, seq, 3, s.cfg_log_dreq);
memcpy(m + 8, b + 8, 8); // correctionField of the Delay_Req
wr_ts(m + 34, mac_ns(&t4));
memcpy(m + 44, b + 20, 10); // requestingPortIdentity
// Unicast Delay_Req (hybrid TimeReceiver) -> unicast Delay_Resp; multicast -> multicast.
bool unicast = rd16(b + 6) & FLAG_UNICAST;
if (unicast) {
m[6] |= FLAG_UNICAST >> 8;
}
send_general(m, sizeof(m), unicast ? src_ip : mcast_ip());
LOCKED({
s.delay_req++;
s.delay_resp++;
});
}
static void become_master(void)
{
char id[24];
fmt_id(id, s.own.gm_id);
ESP_LOGI(TAG, "no better TimeTransmitter: this device is TimeTransmitter %s (p1 %u class %u p2 %u)",
id, s.own.p1, s.own.cls, s.own.p2);
LOCKED({
memset(&s.gm, 0, sizeof(s.gm));
s.master = true;
s.delay_ns = s.prev_t2 = 0;
s.stepped = false;
set_locked(false); // servo state; the frequency correction stays (holdover)
s.last_announce_us = s.prev_t1_tx = 0;
win_clear(&s.announce_iv);
win_clear(&s.sync_iv);
win_clear(&s.delays);
});
s.sync_pending = s.dreq_pending = false;
s.next_sync_us = s.next_announce_us = esp_timer_get_time();
}
static void enter_listening(void)
{
s.listen_since_us = esp_timer_get_time();
}
/* ----- task ----- */ /* ----- task ----- */
static void load_config(void) static void load_config(void)
@@ -417,11 +619,29 @@ static void load_config(void)
s.domain = cJSON_GetObjectItem(c, "domain")->valueint; s.domain = cJSON_GetObjectItem(c, "domain")->valueint;
s.dscp = cJSON_GetObjectItem(c, "dscp")->valueint; s.dscp = cJSON_GetObjectItem(c, "dscp")->valueint;
s.timeout = cJSON_GetObjectItem(c, "announce_timeout")->valueint; s.timeout = cJSON_GetObjectItem(c, "announce_timeout")->valueint;
// clockClass per role: slave-only 255, auto/master 248 (TimeTransmitter itself: step 6) s.cfg_log_sync = cJSON_GetObjectItem(c, "log_sync")->valueint;
s.own_class = strcmp(cJSON_GetObjectItem(c, "role")->valuestring, "slave") == 0 ? 255 : 248; 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).
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;
s.own = (master_t){
.valid = true, .p1 = cJSON_GetObjectItem(c, "priority1")->valueint, .cls = s.own_class,
.acc = 0xFE, .var = 0xFFFF, .p2 = cJSON_GetObjectItem(c, "priority2")->valueint, .steps = 0,
.log_announce = s.cfg_log_announce,
};
memcpy(s.own.gm_id, s.port_id, 8);
memcpy(s.own.port_id, s.port_id, 10);
cJSON_Delete(c); cJSON_Delete(c);
} }
void ptp_clock_reconfig(void)
{
s.reconfig = true;
}
static void ptp_task(void *arg) static void ptp_task(void *arg)
{ {
esp_netif_ip_info_t ip = { 0 }; esp_netif_ip_info_t ip = { 0 };
@@ -434,9 +654,18 @@ static void ptp_task(void *arg)
if (s.ev < 0 || s.gen < 0) { if (s.ev < 0 || s.gen < 0) {
vTaskDelete(NULL); vTaskDelete(NULL);
} }
// General messages we send as TimeTransmitter: subnet only, PTP DSCP, no loopback.
uint8_t ttl = 1, loop = 0;
int tos = s.dscp << 2;
setsockopt(s.gen, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
setsockopt(s.gen, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop));
setsockopt(s.gen, IPPROTO_IP, IP_MULTICAST_IF, &ifaddr, sizeof(ifaddr));
setsockopt(s.gen, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
enter_listening();
char id[24]; char id[24];
fmt_id(id, s.port_id); fmt_id(id, s.port_id);
ESP_LOGI(TAG, "TimeReceiver on " IPSTR ", domain %u, clock %s, listening", IP2STR(&ip.ip), s.domain, id); ESP_LOGI(TAG, "PTP on " IPSTR ", domain %u, clock %s, role %s, listening", IP2STR(&ip.ip), s.domain, id,
s.slave_only ? "TimeReceiver only" : "auto/TimeTransmitter capable");
uint8_t b[128]; uint8_t b[128];
while (1) { while (1) {
@@ -444,7 +673,14 @@ static void ptp_task(void *arg)
FD_ZERO(&fds); FD_ZERO(&fds);
FD_SET(s.ev, &fds); FD_SET(s.ev, &fds);
FD_SET(s.gen, &fds); FD_SET(s.gen, &fds);
struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 }; // Sleep until the next message is due as TimeTransmitter, at most 100 ms.
int64_t wait = 100000;
if (s.master) {
int64_t now = esp_timer_get_time();
int64_t due = s.next_sync_us < s.next_announce_us ? s.next_sync_us : s.next_announce_us;
wait = due - now < 0 ? 0 : due - now < wait ? due - now : wait;
}
struct timeval tv = { .tv_sec = 0, .tv_usec = wait };
if (select((s.ev > s.gen ? s.ev : s.gen) + 1, &fds, NULL, NULL, &tv) > 0) { if (select((s.ev > s.gen ? s.ev : s.gen) + 1, &fds, NULL, NULL, &tv) > 0) {
for (int k = 0; k < 2; k++) { for (int k = 0; k < 2; k++) {
int fd = k ? s.gen : s.ev; int fd = k ? s.gen : s.ev;
@@ -462,16 +698,58 @@ static void ptp_task(void *arg)
case PTP_MSG_SYNC: on_sync(b, len); break; case PTP_MSG_SYNC: on_sync(b, len); break;
case PTP_MSG_FOLLOW_UP: on_follow_up(b, len); break; case PTP_MSG_FOLLOW_UP: on_follow_up(b, len); break;
case PTP_MSG_DELAY_RESP: on_delay_resp(b, len); break; case PTP_MSG_DELAY_RESP: on_delay_resp(b, len); break;
case PTP_MSG_DELAY_REQ: on_delay_req(b, len, src.sin_addr.s_addr); break;
default: break; default: break;
} }
} }
} }
if (s.reconfig) {
s.reconfig = false;
bool was_slave_only = s.slave_only;
load_config();
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.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.
if (s.master && s.slave_only) {
s.master = false;
ESP_LOGI(TAG, "role slave: leaving TimeTransmitter state");
enter_listening();
} else if (s.gm.valid && !s.slave_only && compare(&s.gm, &s.own) > 0) {
become_master();
} else if (!s.gm.valid && was_slave_only != s.slave_only) {
enter_listening();
}
}
int64_t now = esp_timer_get_time(); int64_t now = esp_timer_get_time();
if (s.gm.valid) { if (s.master) {
if (now >= s.next_announce_us) {
send_announce();
s.next_announce_us += log_us(s.cfg_log_announce);
if (s.next_announce_us < now) {
s.next_announce_us = now + log_us(s.cfg_log_announce);
}
}
if (now >= s.next_sync_us) {
send_sync();
s.next_sync_us += log_us(s.cfg_log_sync);
if (s.next_sync_us < now) {
s.next_sync_us = now + log_us(s.cfg_log_sync);
}
}
} else if (!s.gm.valid) {
// LISTENING: no better TimeTransmitter within announceReceiptTimeout -> become one
if (!s.slave_only && now - s.listen_since_us > (int64_t)s.timeout * log_us(s.cfg_log_announce)) {
become_master();
}
} else if (s.gm.valid) {
// announceReceiptTimeout x the GM's announce interval // announceReceiptTimeout x the GM's announce interval
int64_t window = (int64_t)s.timeout * (s.gm.log_announce >= 0 ? 1000000LL << s.gm.log_announce int64_t window = (int64_t)s.timeout * log_us(s.gm.log_announce);
: 1000000LL >> -s.gm.log_announce);
if (now - s.gm.last_us > window) { if (now - s.gm.last_us > window) {
ESP_LOGW(TAG, "TimeTransmitter lost (no Announce for %lld ms), listening", window / 1000); ESP_LOGW(TAG, "TimeTransmitter lost (no Announce for %lld ms), listening", window / 1000);
LOCKED({ LOCKED({
@@ -480,10 +758,11 @@ static void ptp_task(void *arg)
s.stepped = false; s.stepped = false;
set_locked(false); // frequency correction stays (holdover) set_locked(false); // frequency correction stays (holdover)
}); });
enter_listening();
} else if (now >= s.next_dreq_us && s.prev_t2) { } else if (now >= s.next_dreq_us && s.prev_t2) {
send_delay_req(); send_delay_req();
// Delay_Req interval from the GM's Delay_Resp; randomised 0.5..1.5x // Delay_Req interval from the GM's Delay_Resp; randomised 0.5..1.5x
int64_t iv = s.log_dreq >= 0 ? 1000000LL << s.log_dreq : 1000000LL >> -s.log_dreq; int64_t iv = log_us(s.log_dreq);
s.next_dreq_us = now + iv / 2 + (esp_random() % (uint32_t)iv); s.next_dreq_us = now + iv / 2 + (esp_random() % (uint32_t)iv);
} }
} }
@@ -493,17 +772,18 @@ static void ptp_task(void *arg)
bool ptp_clock_gm_id(char out[24]) bool ptp_clock_gm_id(char out[24])
{ {
xSemaphoreTake(s.lock, portMAX_DELAY); xSemaphoreTake(s.lock, portMAX_DELAY);
bool valid = s.gm.valid; bool valid = s.gm.valid || s.master;
if (valid) { if (valid) {
fmt_id(out, s.gm.gm_id); fmt_id(out, s.master ? s.own.gm_id : s.gm.gm_id);
} }
xSemaphoreGive(s.lock); xSemaphoreGive(s.lock);
return valid; return valid;
} }
// Media may be timed from our clock: locked to a GM, or we are the GM.
bool ptp_clock_locked(void) bool ptp_clock_locked(void)
{ {
return s.locked; return s.locked || s.master;
} }
void ptp_clock_status(cJSON *st) void ptp_clock_status(cJSON *st)
@@ -513,9 +793,9 @@ void ptp_clock_status(cJSON *st)
double mean, min, max, sd; double mean, min, max, sd;
xSemaphoreTake(s.lock, portMAX_DELAY); xSemaphoreTake(s.lock, portMAX_DELAY);
const char *state = !s.gm.valid ? "LISTENING" : s.locked ? "SLAVE" : "UNCALIBRATED"; const char *state = s.master ? "MASTER" : !s.gm.valid ? "LISTENING" : s.locked ? "SLAVE" : "UNCALIBRATED";
cJSON_AddStringToObject(p, "state", state); cJSON_AddStringToObject(p, "state", state);
cJSON_AddBoolToObject(p, "locked", s.locked); cJSON_AddBoolToObject(p, "locked", s.locked || s.master);
cJSON_AddNumberToObject(p, "version", 2); cJSON_AddNumberToObject(p, "version", 2);
cJSON_AddNumberToObject(p, "own_class", s.own_class); cJSON_AddNumberToObject(p, "own_class", s.own_class);
fmt_id(id, s.port_id); fmt_id(id, s.port_id);
@@ -524,7 +804,29 @@ void ptp_clock_status(cJSON *st)
cJSON_AddNumberToObject(p, "window", WINDOW); cJSON_AddNumberToObject(p, "window", WINDOW);
cJSON_AddNumberToObject(p, "delay_req", s.delay_req); cJSON_AddNumberToObject(p, "delay_req", s.delay_req);
cJSON_AddNumberToObject(p, "delay_resp", s.delay_resp); cJSON_AddNumberToObject(p, "delay_resp", s.delay_resp);
if (s.gm.valid) { if (s.master) {
// We are the GM: the UI shows "(this device)" and "-" for offset/frequency/delay.
fmt_id(id, s.own.gm_id);
cJSON_AddStringToObject(p, "gm_id", id);
cJSON_AddNumberToObject(p, "gm_class", s.own.cls);
cJSON_AddNumberToObject(p, "gm_accuracy", s.own.acc);
cJSON_AddNumberToObject(p, "gm_p1", s.own.p1);
cJSON_AddNumberToObject(p, "gm_p2", s.own.p2);
cJSON_AddNumberToObject(p, "steps_removed", 0);
cJSON_AddBoolToObject(p, "gm_time_traceable", false);
cJSON_AddBoolToObject(p, "gm_freq_traceable", false);
if (s.sync_iv.n) {
win_stats(&s.sync_iv, &mean, &min, &max, &sd);
cJSON_AddNumberToObject(p, "sync_avg_ms", mean / 1e6);
cJSON_AddNumberToObject(p, "sync_min_ms", min / 1e6);
cJSON_AddNumberToObject(p, "sync_max_ms", max / 1e6);
cJSON_AddNumberToObject(p, "sync_jitter_us", sd / 1e3);
}
if (s.announce_iv.n) {
win_stats(&s.announce_iv, &mean, NULL, NULL, NULL);
cJSON_AddNumberToObject(p, "announce_avg_ms", mean / 1e3);
}
} else if (s.gm.valid) {
fmt_id(id, s.gm.gm_id); fmt_id(id, s.gm.gm_id);
cJSON_AddStringToObject(p, "gm_id", id); cJSON_AddStringToObject(p, "gm_id", id);
cJSON_AddNumberToObject(p, "gm_class", s.gm.cls); cJSON_AddNumberToObject(p, "gm_class", s.gm.cls);
@@ -562,10 +864,10 @@ esp_err_t ptp_clock_start(esp_netif_t *netif)
{ {
s.netif = netif; s.netif = netif;
s.lock = xSemaphoreCreateMutex(); s.lock = xSemaphoreCreateMutex();
load_config();
uint8_t mac[6]; uint8_t mac[6];
esp_netif_get_mac(netif, mac); esp_netif_get_mac(netif, mac);
const uint8_t pid[10] = { mac[0], mac[1], mac[2], 0xff, 0xfe, mac[3], mac[4], mac[5], 0, 1 }; const uint8_t pid[10] = { mac[0], mac[1], mac[2], 0xff, 0xfe, mac[3], mac[4], mac[5], 0, 1 };
memcpy(s.port_id, pid, sizeof(pid)); memcpy(s.port_id, pid, sizeof(pid));
load_config(); // after port_id: the own dataset uses it
return xTaskCreate(ptp_task, "ptp", 4096, NULL, 10, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM; return xTaskCreate(ptp_task, "ptp", 4096, NULL, 10, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
} }
+3 -1
View File
@@ -1,4 +1,4 @@
// PTPv2 ordinary clock over UDP/IPv4 (E2E). TimeReceiver (TimeTransmitter: step 6). // PTPv2 ordinary clock over UDP/IPv4 (E2E). TimeReceiver and TimeTransmitter (BMCA).
#pragma once #pragma once
#include "cJSON.h" #include "cJSON.h"
@@ -6,6 +6,8 @@
#include "esp_netif.h" #include "esp_netif.h"
esp_err_t ptp_clock_start(esp_netif_t *netif); esp_err_t ptp_clock_start(esp_netif_t *netif);
// Re-read the "ptp" config group (role, priorities, intervals, domain, DSCP) in the PTP task.
void ptp_clock_reconfig(void);
bool ptp_clock_gm_id(char out[24]); bool ptp_clock_gm_id(char out[24]);
bool ptp_clock_locked(void); bool ptp_clock_locked(void);
// Adds the "ptp" object to /api/status. // Adds the "ptp" object to /api/status.
+14
View File
@@ -18,6 +18,7 @@ typedef struct {
uint8_t type; uint8_t type;
uint16_t seq; uint16_t seq;
uint8_t port_id[10]; uint8_t port_id[10];
uint8_t mac[6]; // Ethernet source
eth_mac_time_t ts; eth_mac_time_t ts;
} rx_rec_t; } rx_rec_t;
@@ -48,6 +49,7 @@ static esp_err_t rx_hook(esp_eth_handle_t eth, uint8_t *buf, uint32_t len, void
r->type = ptp[0] & 0x0f; r->type = ptp[0] & 0x0f;
r->seq = (ptp[30] << 8) | ptp[31]; r->seq = (ptp[30] << 8) | ptp[31];
memcpy(r->port_id, ptp + 20, 10); memcpy(r->port_id, ptp + 20, 10);
memcpy(r->mac, buf + 6, 6);
r->ts = *ts; r->ts = *ts;
portEXIT_CRITICAL(&s_lock); portEXIT_CRITICAL(&s_lock);
} }
@@ -56,6 +58,12 @@ static esp_err_t rx_hook(esp_eth_handle_t eth, uint8_t *buf, uint32_t len, void
} }
bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts) bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts)
{
return ptp_hw_rx_ts_mac(msg_type, seq, src_port_id, ts, NULL);
}
bool ptp_hw_rx_ts_mac(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts,
uint8_t src_mac[6])
{ {
bool found = false; bool found = false;
portENTER_CRITICAL(&s_lock); portENTER_CRITICAL(&s_lock);
@@ -63,6 +71,9 @@ bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, et
rx_rec_t *r = &s_rx[i]; rx_rec_t *r = &s_rx[i];
if (r->used && r->type == msg_type && r->seq == seq && memcmp(r->port_id, src_port_id, 10) == 0) { if (r->used && r->type == msg_type && r->seq == seq && memcmp(r->port_id, src_port_id, 10) == 0) {
*ts = r->ts; *ts = r->ts;
if (src_mac) {
memcpy(src_mac, r->mac, 6);
}
r->used = false; r->used = false;
found = true; found = true;
break; break;
@@ -181,6 +192,9 @@ esp_err_t ptp_hw_init(esp_eth_handle_t eth, esp_netif_t *netif)
} }
// IDF enables timestamping for PTP over Ethernet (L2) only; AES67 uses UDP/IPv4. // IDF enables timestamping for PTP over Ethernet (L2) only; AES67 uses UDP/IPv4.
emac_ll_ts_ptp_ip4_enable(&EMAC_PTP, true); emac_ll_ts_ptp_ip4_enable(&EMAC_PTP, true);
// The PTP packet filter only stamps multicast PTP; unicast Delay_Req (hybrid mode) would get
// no timestamp. Stamp every frame instead; rx_hook picks the PTP event messages.
emac_ll_ts_all_enable(&EMAC_PTP, true);
// The netif glue registered its own input path; take it over and forward to the netif. // The netif glue registered its own input path; take it over and forward to the netif.
err = esp_eth_update_input_path_info(eth, rx_hook, netif); err = esp_eth_update_input_path_info(eth, rx_hook, netif);
+3
View File
@@ -19,6 +19,9 @@ esp_err_t ptp_hw_init(esp_eth_handle_t eth, esp_netif_t *netif);
// Hardware RX timestamp of an event message (UDP port 319), looked up by type, // Hardware RX timestamp of an event message (UDP port 319), looked up by type,
// sequence ID and sourcePortIdentity (10 bytes). Each record is returned once. // sequence ID and sourcePortIdentity (10 bytes). Each record is returned once.
bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts); bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts);
// Same, also returning the sender's Ethernet MAC (for unicast replies as raw frames). src_mac may be NULL.
bool ptp_hw_rx_ts_mac(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts,
uint8_t src_mac[6]);
esp_err_t ptp_hw_get_time(eth_mac_time_t *t); esp_err_t ptp_hw_get_time(eth_mac_time_t *t);
// Frequency correction relative to the nominal rate, in ppb (positive = faster). // Frequency correction relative to the nominal rate, in ppb (positive = faster).
esp_err_t ptp_hw_adj_freq(double ppb); esp_err_t ptp_hw_adj_freq(double ppb);
+21
View File
@@ -164,6 +164,26 @@ static bool sap_wanted(uint8_t *ttl)
return on; return on;
} }
// When the PTP GM changes, the SDP's ts-refclk changes: bump the session version (RFC 4566 o=)
// so receivers see a new description. Runs whether or not SAP is on (manual SDP users too).
static void check_gm_change(void)
{
static char last[24];
char gm[24];
if (!aes67_ptp_gm_id(gm) || strcmp(gm, last) == 0) {
return;
}
if (last[0]) {
cJSON *a = cfg_get("aes67");
double ver = cJSON_GetObjectItemCaseSensitive(a, "session_ver")->valuedouble + 1;
cJSON_Delete(a);
// Not applied: re-applying the aes67 group would restart the stream.
esp_err_t err = cfg_set_number("aes67", "session_ver", ver, false);
ESP_LOGI(TAG, "PTP TimeTransmitter %s -> %s: session_ver %.0f (%s)", last, gm, ver, esp_err_to_name(err));
}
strcpy(last, gm);
}
static void sap_task(void *arg) static void sap_task(void *arg)
{ {
static char sdp[SDP_MAX]; static char sdp[SDP_MAX];
@@ -172,6 +192,7 @@ static void sap_task(void *arg)
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
check_gm_change();
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_netif_t *netif = aes67_net_netif(); esp_netif_t *netif = aes67_net_netif();
bool have_ip = netif && esp_netif_get_ip_info(netif, &ip) == ESP_OK && ip.ip.addr; bool have_ip = netif && esp_netif_get_ip_info(netif, &ip) == ESP_OK && ip.ip.addr;
+31
View File
@@ -232,6 +232,37 @@ cJSON *cfg_get(const char *group)
return copy; return copy;
} }
esp_err_t cfg_set_number(const char *group, const char *key, double 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_IsNumber(cur)) {
err = ESP_ERR_NOT_FOUND;
} else {
cJSON_SetNumberValue(cur, 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)
+3
View File
@@ -20,6 +20,9 @@ esp_err_t cfg_register(const char *group, const char *defaults_json,
cfg_validate_cb_t validate, cfg_apply_cb_t apply); cfg_validate_cb_t validate, cfg_apply_cb_t apply);
// Copy of a group's current values; caller frees with cJSON_Delete. NULL if not registered. // Copy of a group's current values; caller frees with cJSON_Delete. NULL if not registered.
cJSON *cfg_get(const char *group); 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);
// 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);
+12 -1
View File
@@ -60,6 +60,11 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct
- RX timestamps: a hook on the driver's info input path (`esp_eth_update_input_path_info`) records port-319 event timestamps and forwards every frame to lwIP. TX: Delay_Req is a raw Eth/IPv4/UDP frame sent with `esp_eth_transmit_ctrl_vargs` for its HW timestamp. Needs `CONFIG_ETH_TRANSMIT_MUTEX`. - RX timestamps: a hook on the driver's info input path (`esp_eth_update_input_path_info`) records port-319 event timestamps and forwards every frame to lwIP. TX: Delay_Req is a raw Eth/IPv4/UDP frame sent with `esp_eth_transmit_ctrl_vargs` for its HW timestamp. Needs `CONFIG_ETH_TRANSMIT_MUTEX`.
- Servo: step on the first Sync from a new GM (frequency seeded from the measured rate), then linuxptp-style PI (kp 0.7 / ki 0.3 at 1 Sync/s, scaled by the interval); re-step above 1 ms. Locked: 8 Syncs below 1 µs; unlocked after 3 above. GM loss keeps the frequency (holdover). - Servo: step on the first Sync from a new GM (frequency seeded from the measured rate), then linuxptp-style PI (kp 0.7 / ki 0.3 at 1 Sync/s, scaled by the interval); re-step above 1 ms. Locked: 8 Syncs below 1 µs; unlocked after 3 above. GM loss keeps the frequency (holdover).
- Path delay is corrected for offset drift between t2 and t3 until the clock is syntonised; delay statistics start at lock. - Path delay is corrected for offset drift between t2 and t3 until the clock is syntonised; delay statistics start at lock.
- TimeTransmitter/BMCA (step 6): own dataset from the role; a foreign GM is followed only if better; LISTENING -> MASTER after the announce receipt timeout. Two-step Sync (raw frame, HW TX time in Follow_Up), Announce with the PTP timescale flag, Delay_Resp with HW RX time. Hybrid: as TimeReceiver, Delay_Req unicast to the GM (IP from Announce, MAC from its Sync) with the unicastFlag; as TimeTransmitter, a unicast Delay_Req gets a unicast Delay_Resp.
- The EMAC's PTP filter only timestamps multicast PTP; unicast Delay_Req got none. The EMAC now timestamps every received frame (`emac_ll_ts_all_enable`); the RX hook picks the port-319 PTP event messages.
- A unicast Delay_Resp carries logMessageInterval 0x7F; then ptp.log_delay_req is used.
- As GM without an earlier lock the clock starts at 0 (1970): no RTC/NTP. Media timing is unaffected; NTP seeding could follow with the internet interface (phase 2).
- Sync send times jitter by ~10 ms (FreeRTOS 100 Hz tick); accuracy is unaffected (two-step).
- Measured vs ptp4l (Intel i210 GM, non-PTP switch): lock in ~35 s cold, ~22 s after GM loss; offset within a few hundred ns; board crystal -39.8 ppm. Link asymmetry (1G GM / 100M board through a store-and-forward switch) adds a constant offset error of a few µs that no receiver can see. - Measured vs ptp4l (Intel i210 GM, non-PTP switch): lock in ~35 s cold, ~22 s after GM loss; offset within a few hundred ns; board crystal -39.8 ppm. Link asymmetry (1G GM / 100M board through a store-and-forward switch) adds a constant offset error of a few µs that no receiver can see.
### PTP status (`status.ptp`) — main panel modelled on Riedel Bolero "PTP Status" ### PTP status (`status.ptp`) — main panel modelled on Riedel Bolero "PTP Status"
@@ -89,7 +94,7 @@ When this device is the GM, offset/frequency/delay show "–".
- SDP: v, o (aes67 IP, session_id, session_ver), s, c=mcast/ttl, t=0 0, a=clock-domain:PTPv2 <dom>, m=audio port RTP/AVP pt, a=rtpmap, a=recvonly, a=ptime, a=ts-refclk:ptp=IEEE1588-2008:<gm>:<dom>, a=mediaclk:direct. - SDP: v, o (aes67 IP, session_id, session_ver), s, c=mcast/ttl, t=0 0, a=clock-domain:PTPv2 <dom>, m=audio port RTP/AVP pt, a=rtpmap, a=recvonly, a=ptime, a=ts-refclk:ptp=IEEE1588-2008:<gm>:<dom>, a=mediaclk:direct.
- SAP to 239.255.255.255:9875 every 30 s, plus immediately on change; deletion packet on disable or shutdown. - SAP to 239.255.255.255:9875 every 30 s, plus immediately on change; deletion packet on disable or shutdown.
- Implemented (aes67_sdp_sap): SAPv1, payload type `application/sdp`, msg id hash = 16-bit hash of the SDP, TTL = aes67.ttl. The SDP is checked once a second; on a change the old hash is deleted before the new one is announced. Nothing is announced until a PTP GM is known (else ts-refclk would be all zeros). Shutdown deletion via `esp_register_shutdown_handler` (covers reboot and OTA). - Implemented (aes67_sdp_sap): SAPv1, payload type `application/sdp`, msg id hash = 16-bit hash of the SDP, TTL = aes67.ttl. The SDP is checked once a second; on a change the old hash is deleted before the new one is announced. Nothing is announced until a PTP GM is known (else ts-refclk would be all zeros). Shutdown deletion via `esp_register_shutdown_handler` (covers reboot and OTA).
- Not done yet: bumping session_ver on a GM change (needs a firmware-side config write; relevant once the device can become GM, step 6). A GM change already re-announces immediately with the new ts-refclk. - GM change: aes67.session_ver is bumped (stored via `cfg_set_number`, without re-applying the aes67 group so the stream keeps running) and SAP re-announces within 1 s. The first GM after boot is not counted as a change.
- Media 2 (ST 2022-7 redundancy): not possible on single-port boards like the ESP32-P4-ETH. Keep the schema open for a `mcast2`/`port2` pair on dual-NIC hardware (Riedel applies one PTP config to both Media 1 and Media 2). - Media 2 (ST 2022-7 redundancy): not possible on single-port boards like the ESP32-P4-ETH. Keep the schema open for a `mcast2`/`port2` pair on dual-NIC hardware (Riedel applies one PTP config to both Media 1 and Media 2).
## AES67 RX (future, for receiver projects) — Riedel Director 4-wire input defaults ## AES67 RX (future, for receiver projects) — Riedel Director 4-wire input defaults
@@ -105,6 +110,12 @@ When this device is the GM, offset/frequency/delay show "–".
- LLDP (IEEE 802.1AB), transmit only, own code (not in IDF/lwIP): every 30 s, TTL 120 s, plus immediately on a new IP or hostname. TLVs: chassis ID and port ID = MAC, port description "eth0", system name = net.hostname, system description = project + firmware version, capabilities station-only, management address = IPv4. With the VLAN split, send it on the AES67 (untagged) side; the 802.1 port VLAN ID TLV can be added then. - LLDP (IEEE 802.1AB), transmit only, own code (not in IDF/lwIP): every 30 s, TTL 120 s, plus immediately on a new IP or hostname. TLVs: chassis ID and port ID = MAC, port description "eth0", system name = net.hostname, system description = project + firmware version, capabilities station-only, management address = IPv4. With the VLAN split, send it on the AES67 (untagged) side; the 802.1 port VLAN ID TLV can be added then.
- Switch port: AES67 as native/untagged VLAN, internet tagged, PoE on. IGMP snooping + querier on the AES67 VLAN. - Switch port: AES67 as native/untagged VLAN, internet tagged, PoE on. IGMP snooping + querier on the AES67 VLAN.
## Time / NTP (phase 2, not implemented yet)
- SNTP client for wall-clock time. Servers configurable as hostnames or IPs (e.g. `pool.ntp.org`, `0.pool.ntp.org`, a local server); several allowed, tried in order; names resolved via DNS and re-resolved when a server stops answering.
- Uses: set the EMAC PTP clock to real time (TAI = UTC + 37 s) before this device becomes GM when it has not learned time from another GM (without NTP it starts at 1970); RFC 5424/3164 syslog timestamps.
- Never step the PTP clock from NTP while locked to a GM or while GM with receivers locked; only seed it before becoming GM.
- Planned config group: time: {ntp, servers[] or a comma-separated string, sync_interval_s}; the UI needs matching fields.
## Syslog ## Syslog
- esp_log vprintf hook (chains to UART) -> queue -> low-priority UDP task. Non-blocking, drop and count on overflow, no recursion. RFC 5424 or 3164, PRI = facility*8 + severity (E3 W4 I6 D7 V7). Strip ANSI colour codes. HOSTNAME = net.hostname, APP-NAME = log tag. - esp_log vprintf hook (chains to UART) -> queue -> low-priority UDP task. Non-blocking, drop and count on overflow, no recursion. RFC 5424 or 3164, PRI = facility*8 + severity (E3 W4 I6 D7 V7). Strip ANSI colour codes. HOSTNAME = net.hostname, APP-NAME = log tag.
- Buffer early boot logs in the queue until the netif has an IP. POST /api/log/test sends one info-level message. - Buffer early boot logs in the queue until the netif has an IP. POST /api/log/test sends one info-level message.
+5
View File
@@ -19,3 +19,8 @@ CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y
# LLDP and PTP send raw frames from their own tasks next to lwIP: serialise EMAC transmits # LLDP and PTP send raw frames from their own tasks next to lwIP: serialise EMAC transmits
CONFIG_ETH_TRANSMIT_MUTEX=y CONFIG_ETH_TRANSMIT_MUTEX=y
# lwIP default is 10 sockets; PTP (2), AES67 TX, SAP, syslog and httpd's listen/control sockets
# use 7, leaving ~3 for web clients (httpd allows 7): a browser's keep-alive connections then
# starve new requests. 16 = 7 others + 7 web clients + OTA self-test client + 1 spare.
CONFIG_LWIP_MAX_SOCKETS=16