diff --git a/components/aes67_ptp/ptp_clock.c b/components/aes67_ptp/ptp_clock.c index d82c8d4..0cf2e11 100644 --- a/components/aes67_ptp/ptp_clock.c +++ b/components/aes67_ptp/ptp_clock.c @@ -26,6 +26,7 @@ #define MAX_DRIFT_PPB 500000.0 #define WINDOW 64 // samples for interval/delay statistics #define SUMMARY_US (60 * 1000000LL) +#define AVG_S 120 // status.ptp.offset_avg_ns: mean |offset| over 2 min #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 @@ -102,8 +103,48 @@ static struct { uint32_t delay_req, delay_resp; uint8_t own_class; int64_t sum_max_ns, sum_start_us; + // Mean |offset| over the last AVG_S seconds (status.ptp.offset_avg_ns): one bucket per second. + struct { + int64_t sec, sum; + uint32_t n; + } avg[AVG_S]; } s; +static void avg_add(int64_t offset) +{ + int64_t sec = esp_timer_get_time() / 1000000; + typeof(s.avg[0]) *b = &s.avg[sec % AVG_S]; + if (b->sec != sec) { + b->sec = sec; + b->sum = 0; + b->n = 0; + } + b->sum += llabs(offset); + b->n++; +} + +static void avg_clear(void) +{ + memset(s.avg, 0, sizeof(s.avg)); +} + +// False while no offset was measured in the window. +static bool avg_get(double *mean) +{ + int64_t now = esp_timer_get_time() / 1000000, sum = 0; + uint32_t n = 0; + for (int i = 0; i < AVG_S; i++) { + if (s.avg[i].n && now - s.avg[i].sec < AVG_S) { + sum += s.avg[i].sum; + n += s.avg[i].n; + } + } + if (n) { + *mean = (double)sum / n; + } + return n > 0; +} + static void win_add(window_t *w, int64_t v) { w->v[w->next] = v; @@ -309,6 +350,7 @@ static void servo(int64_t offset) s.stepped = true; s.offset_ns = 0; // the pre-step offset is history: not for status or the summary s.sum_max_ns = 0; + avg_clear(); s.sum_start_us = 0; s.prev_t2 = 0; // rate across the step is meaningless s.bad = 0; @@ -323,6 +365,7 @@ static void servo(int64_t offset) s.drift_ppb = fmax(-MAX_DRIFT_PPB, fmin(MAX_DRIFT_PPB, s.drift_ppb + ki * offset)); s.freq_ppb = -ppb; ptp_hw_adj_freq(s.freq_ppb); + avg_add(offset); if (llabs(offset) < LOCK_NS) { s.bad = 0; @@ -838,6 +881,9 @@ void ptp_clock_status(cJSON *st) cJSON_AddBoolToObject(p, "gm_freq_traceable", s.gm.flags & 0x20); if (s.stepped) { cJSON_AddNumberToObject(p, "offset_ns", s.offset_ns); + if (avg_get(&mean)) { + cJSON_AddNumberToObject(p, "offset_avg_ns", round(mean)); + } cJSON_AddNumberToObject(p, "freq_ppb", round(s.freq_ppb)); } if (s.delays.n) { diff --git a/docs/aes67-core-base.md b/docs/aes67-core-base.md index 83e8fd9..0d2c2da 100644 --- a/docs/aes67-core-base.md +++ b/docs/aes67-core-base.md @@ -15,7 +15,7 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct - 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 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). +- PTP preset buttons: "Riedel PTP defaults" and "AES67 Media PTP defaults" fill the form (Save to apply). ## Config schema (core groups) - 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) @@ -76,15 +76,11 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct | PTP State | state | INITIALIZING/LISTENING/UNCALIBRATED/SLAVE/MASTER/PASSIVE/FAULTY/DISABLED, shown as TimeReceiver/TimeTransmitter/… | | Lock State | locked | servo converged (e.g. \|offset\| < 1 µs for N consecutive syncs) | | TimeTransmitter | gm_id | shown as MAC + "(FFFE)" when EUI-64 from MAC | -| Time Offset | offset_ns | Bolero brackets in ns: <100 (green), 100-500 (green), 500-1000 (amber), 1000-10000 (red), >10000 (red); exact value in brackets | +| Time Offset (2 min avg) | offset_avg_ns | mean \|offset\| over the last 2 min (reset on a clock step); Bolero brackets in ns: <100 (green), 100-500 (green), 500-1000 (amber), 1000-10000 (red), >10000 (red). The current offset_ns is in Details | | Frequency Deviation | freq_ppb | servo frequency correction; buckets <100 ppb, 100-500 ppb, 500 ppb-1 ppm, 1-10 ppm, 10-50 ppm, >50 ppm | | Network Delay | path_delay_ns ± path_delay_sd_ns | mean path delay ± std dev over the rolling window | -| Hops | steps_removed | | -| Time/Frequency Traceable | gm_time_traceable, gm_freq_traceable | Announce flagField bits | -| Version | version | 2 | -| Own Clock Class | own_class | 255 / 248 per role | -Details (collapsed): clock_id, gm_class, gm_accuracy, gm_p1, gm_p2, sync_avg_ms/min/max + sync_jitter_us (measured Sync interval over the last `window` = 64 messages; RX as receiver, TX as transmitter), announce_avg_ms, delay_req/delay_resp counters, hw_ts. +Details (collapsed): clock_id, current offset_ns, gm_class, gm_accuracy, gm_p1, gm_p2, hops (steps_removed), time/frequency traceable (gm_time_traceable, gm_freq_traceable: Announce flagField bits), version (2), own clock class (own_class: 255 / 248 per role), sync_avg_ms/min/max + sync_jitter_us (measured Sync interval over the last `window` = 64 messages; RX as receiver, TX as transmitter), announce_avg_ms, delay_req/delay_resp counters, hw_ts. When this device is the GM, offset/frequency/delay show "–". ## AES67 TX diff --git a/docs/user-guide.md b/docs/user-guide.md index 196ff7b..499cdae 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -29,7 +29,7 @@ The page is split into sections, from top to bottom: | Section | What it's for | |---|---| | **Status** | Firmware version, IP / MAC, link speed, active source, Spotify state, buffer, packets sent, underruns, uptime, memory, chip temperature. | -| **PTP status** | Whether the clock is locked, which grandmaster it follows, offset and path delay. *Details* shows more. | +| **PTP status** | Whether the clock is locked, which grandmaster it follows, the time offset (bracket of the 2-minute average) and path delay. *Details* shows the current offset, hops and more. | | **Player** | What's playing now, transport buttons, progress bar (drag to seek), volume. | | **Source**, **PTP**, **AES67 output**, **Network**, **Logging** | Settings. They take effect only when you click **Save**. | | **SDP** | The stream description, to copy or download for receivers. | @@ -129,8 +129,8 @@ The board locks its media clock to the PTP grandmaster on the network (IEEE 1588 | Intervals, timeout, DSCP (46) | | Presets below set them. | **Presets:** -- **Riedel SIC defaults:** TimeReceiver only, priorities 128/126, Riedel's intervals. Use this when a Riedel system provides the clock. -- **AES67 media profile defaults:** the faster intervals of the AES67 profile. +- **Riedel PTP defaults:** TimeReceiver only, priorities 128/126, Riedel's intervals. Use this when a Riedel system provides the clock. +- **AES67 Media PTP defaults:** the faster intervals of the AES67 profile. A preset only fills in the fields; click **Save** to apply them. diff --git a/web/index.html b/web/index.html index c2d8bc5..20e3764 100644 --- a/web/index.html +++ b/web/index.html @@ -98,8 +98,8 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next