diff --git a/components/aes67_ptp/aes67_ptp.c b/components/aes67_ptp/aes67_ptp.c index e882171..97e3132 100644 --- a/components/aes67_ptp/aes67_ptp.c +++ b/components/aes67_ptp/aes67_ptp.c @@ -1,6 +1,7 @@ #include "aes67_ptp.h" #include "aes67_cfg.h" +#include "aes67_web.h" #include "ptp_clock.h" #include "ptp_hw.h" @@ -38,5 +39,9 @@ esp_err_t aes67_ptp_start(esp_eth_handle_t eth) if (err != ESP_OK) { return err; } - return ptp_clock_start(netif); + err = ptp_clock_start(netif); + if (err == ESP_OK) { + err = status_register(ptp_clock_status); + } + return err; } diff --git a/components/aes67_ptp/ptp_clock.c b/components/aes67_ptp/ptp_clock.c index 2640a40..ccd26ae 100644 --- a/components/aes67_ptp/ptp_clock.c +++ b/components/aes67_ptp/ptp_clock.c @@ -9,6 +9,7 @@ #include "esp_random.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "lwip/sockets.h" #include "ptp_hw.h" @@ -23,10 +24,18 @@ #define LOCK_GOOD 8 // consecutive good Syncs to lock #define LOCK_BAD 3 // consecutive bad Syncs to unlock #define MAX_DRIFT_PPB 500000.0 +#define WINDOW 64 // samples for interval/delay statistics +#define SUMMARY_US (60 * 1000000LL) static const char *TAG = "ptp"; static const uint8_t PTP_MCAST_MAC[6] = { 0x01, 0x00, 0x5e, 0x00, 0x01, 0x81 }; +// Rolling window of int64 samples. +typedef struct { + int64_t v[WINDOW]; + int n, next; +} window_t; + typedef struct { bool valid; uint8_t port_id[10]; // sourcePortIdentity of the GM (clockId + port) @@ -35,6 +44,7 @@ typedef struct { uint16_t var; uint8_t gm_id[8]; uint16_t steps; + uint8_t flags; // flagField octet 1: timeTraceable 0x10, frequencyTraceable 0x20 int8_t log_announce; int64_t last_us; // last Announce (esp_timer) } master_t; @@ -68,8 +78,49 @@ static struct { int64_t offset_ns; int good, bad; bool locked; + // Statistics for status.ptp (guarded by lock) + SemaphoreHandle_t lock; + window_t sync_iv, announce_iv, delays; + int64_t last_announce_us; + uint32_t delay_req, delay_resp; + uint8_t own_class; + int64_t sum_max_ns, sum_start_us; } s; +static void win_add(window_t *w, int64_t v) +{ + w->v[w->next] = v; + w->next = (w->next + 1) % WINDOW; + if (w->n < WINDOW) { + w->n++; + } +} + +static void win_clear(window_t *w) +{ + w->n = w->next = 0; +} + +// mean, min, max and standard deviation of a window +static void win_stats(const window_t *w, double *mean, double *min, double *max, double *sd) +{ + double sum = 0, sq = 0, lo = 0, hi = 0; + for (int i = 0; i < w->n; i++) { + double x = (double)w->v[i]; + sum += x; + sq += x * x; + lo = i ? fmin(lo, x) : x; + hi = i ? fmax(hi, x) : x; + } + double m = w->n ? sum / w->n : 0; + *mean = m; + if (min) *min = lo; + if (max) *max = hi; + if (sd) *sd = w->n > 1 ? sqrt(fmax(0, sq / w->n - m * m)) : 0; +} + +#define LOCKED(stmt) do { xSemaphoreTake(s.lock, portMAX_DELAY); stmt; xSemaphoreGive(s.lock); } while (0) + static void set_locked(bool locked); /* ----- helpers ----- */ @@ -146,7 +197,8 @@ static void on_announce(const uint8_t *b, int len, uint32_t src_ip) } master_t m = { .valid = true, .ip = src_ip, .p1 = b[47], .cls = b[48], .acc = b[49], .var = rd16(b + 50), - .p2 = b[52], .steps = rd16(b + 61), .log_announce = (int8_t)b[33], .last_us = esp_timer_get_time(), + .p2 = b[52], .steps = rd16(b + 61), .flags = b[7], .log_announce = (int8_t)b[33], + .last_us = esp_timer_get_time(), }; memcpy(m.port_id, b + 20, 10); memcpy(m.gm_id, b + 53, 8); @@ -154,17 +206,31 @@ static void on_announce(const uint8_t *b, int len, uint32_t src_ip) return; // our own } if (from_gm(b)) { - s.gm = m; // refresh dataset and timeout + LOCKED({ + if (s.last_announce_us) { + win_add(&s.announce_iv, m.last_us - s.last_announce_us); + } + s.last_announce_us = m.last_us; + s.gm = m; // refresh dataset and timeout + }); } else if (!s.gm.valid || compare(&m, &s.gm) < 0) { char id[24]; fmt_id(id, m.gm_id); ESP_LOGI(TAG, "TimeTransmitter %s (p1 %u class %u p2 %u, %u hops) from " IPSTR, id, m.p1, m.cls, m.p2, m.steps, IP2STR((esp_ip4_addr_t *)&src_ip)); - s.gm = m; + LOCKED({ + s.gm = m; + s.last_announce_us = m.last_us; + win_clear(&s.announce_iv); + win_clear(&s.sync_iv); + win_clear(&s.delays); + }); s.sync_pending = s.dreq_pending = false; - s.delay_ns = s.prev_t2 = 0; - s.stepped = false; - set_locked(false); + LOCKED({ + s.delay_ns = s.prev_t2 = 0; + s.stepped = false; + set_locked(false); + }); s.log_dreq = 0; s.next_dreq_us = 0; } @@ -175,6 +241,7 @@ static void set_locked(bool locked) if (locked != s.locked) { s.locked = locked; if (locked) { + win_clear(&s.delays); // delay statistics cover the locked period, not the pull-in ESP_LOGI(TAG, "locked: offset %+lld ns, frequency %+.3f ppm, path delay %lld ns", s.offset_ns, s.freq_ppb / 1000, s.delay_ns); } else { @@ -234,6 +301,7 @@ static void sync_complete(void) s.raw = s.t2 - s.t1 - s.sync_corr; if (s.prev_t2 && s.t2 > s.prev_t2) { s.rate = (double)(s.raw - s.prev_raw) / (double)(s.t2 - s.prev_t2); + LOCKED(win_add(&s.sync_iv, s.t2 - s.prev_t2)); } s.prev_raw = s.raw; s.prev_t2 = s.t2; @@ -241,9 +309,20 @@ static void sync_complete(void) return; } // offset = t2 - t1 - corrections - mean path delay - servo(s.raw - s.delay_ns); - ESP_LOGI(TAG, "seq %u: offset %+lld ns, freq %+.3f ppm, path delay %lld ns%s", s.sync_seq, s.offset_ns, + LOCKED(servo(s.raw - s.delay_ns)); + ESP_LOGD(TAG, "seq %u: offset %+lld ns, freq %+.3f ppm, path delay %lld ns%s", s.sync_seq, s.offset_ns, s.freq_ppb / 1000, s.delay_ns, s.locked ? ", locked" : ""); + // Info-level summary once a minute: worst offset in the period. + int64_t now = esp_timer_get_time(); + s.sum_max_ns = llabs(s.offset_ns) > s.sum_max_ns ? llabs(s.offset_ns) : s.sum_max_ns; + if (!s.sum_start_us) { + s.sum_start_us = now; + } else if (now - s.sum_start_us >= SUMMARY_US) { + ESP_LOGI(TAG, "%s: max |offset| %lld ns, freq %+.3f ppm, path delay %lld ns (last 60 s)", + s.locked ? "locked" : "unlocked", s.sum_max_ns, s.freq_ppb / 1000, s.delay_ns); + s.sum_max_ns = 0; + s.sum_start_us = now; + } } static void on_sync(const uint8_t *b, int len) @@ -303,6 +382,7 @@ static void send_delay_req(void) } s.t3 = mac_ns(&t3); s.dreq_pending = true; + LOCKED(s.delay_req++); } static void on_delay_resp(const uint8_t *b, int len) @@ -312,6 +392,7 @@ static void on_delay_resp(const uint8_t *b, int len) return; } s.dreq_pending = false; + LOCKED(s.delay_resp++); int64_t t4 = rd_ts(b + 34) - rd_corr_ns(b + 8); s.log_dreq = (int8_t)b[33]; if (!s.prev_t2) { @@ -319,7 +400,10 @@ static void on_delay_resp(const uint8_t *b, int len) } // mean path delay = ((t2 - t1 - corr) + (t4 - t3)) / 2, plus the offset drift between t2 and t3 int64_t d = (s.raw + (t4 - s.t3) + (int64_t)(s.rate * (double)(s.t3 - s.t2))) / 2; - s.delay_ns = s.delay_ns ? (s.delay_ns * 7 + d) / 8 : d; // light smoothing + LOCKED({ + s.delay_ns = s.delay_ns ? (s.delay_ns * 7 + d) / 8 : d; // light smoothing for the servo + win_add(&s.delays, d); // raw samples for status + }); } /* ----- task ----- */ @@ -330,6 +414,8 @@ static void load_config(void) s.domain = cJSON_GetObjectItem(c, "domain")->valueint; s.dscp = cJSON_GetObjectItem(c, "dscp")->valueint; s.timeout = cJSON_GetObjectItem(c, "announce_timeout")->valueint; + // clockClass per role: slave-only 255, auto/master 248 (TimeTransmitter itself: step 6) + s.own_class = strcmp(cJSON_GetObjectItem(c, "role")->valuestring, "slave") == 0 ? 255 : 248; cJSON_Delete(c); } @@ -385,10 +471,12 @@ static void ptp_task(void *arg) : 1000000LL >> -s.gm.log_announce); if (now - s.gm.last_us > window) { ESP_LOGW(TAG, "TimeTransmitter lost (no Announce for %lld ms), listening", window / 1000); - memset(&s.gm, 0, sizeof(s.gm)); - s.delay_ns = s.prev_t2 = 0; - s.stepped = false; - set_locked(false); // frequency correction stays (holdover) + LOCKED({ + memset(&s.gm, 0, sizeof(s.gm)); + s.delay_ns = s.prev_t2 = 0; + s.stepped = false; + set_locked(false); // frequency correction stays (holdover) + }); } else if (now >= s.next_dreq_us && s.prev_t2) { send_delay_req(); // Delay_Req interval from the GM's Delay_Resp; randomised 0.5..1.5x @@ -399,9 +487,62 @@ static void ptp_task(void *arg) } } +void ptp_clock_status(cJSON *st) +{ + cJSON *p = cJSON_AddObjectToObject(st, "ptp"); + char id[24]; + double mean, min, max, sd; + + xSemaphoreTake(s.lock, portMAX_DELAY); + const char *state = !s.gm.valid ? "LISTENING" : s.locked ? "SLAVE" : "UNCALIBRATED"; + cJSON_AddStringToObject(p, "state", state); + cJSON_AddBoolToObject(p, "locked", s.locked); + cJSON_AddNumberToObject(p, "version", 2); + cJSON_AddNumberToObject(p, "own_class", s.own_class); + fmt_id(id, s.port_id); + cJSON_AddStringToObject(p, "clock_id", id); + cJSON_AddBoolToObject(p, "hw_ts", true); + cJSON_AddNumberToObject(p, "window", WINDOW); + cJSON_AddNumberToObject(p, "delay_req", s.delay_req); + cJSON_AddNumberToObject(p, "delay_resp", s.delay_resp); + if (s.gm.valid) { + fmt_id(id, s.gm.gm_id); + cJSON_AddStringToObject(p, "gm_id", id); + cJSON_AddNumberToObject(p, "gm_class", s.gm.cls); + cJSON_AddNumberToObject(p, "gm_accuracy", s.gm.acc); + cJSON_AddNumberToObject(p, "gm_p1", s.gm.p1); + cJSON_AddNumberToObject(p, "gm_p2", s.gm.p2); + cJSON_AddNumberToObject(p, "steps_removed", s.gm.steps); + cJSON_AddBoolToObject(p, "gm_time_traceable", s.gm.flags & 0x10); + cJSON_AddBoolToObject(p, "gm_freq_traceable", s.gm.flags & 0x20); + if (s.stepped) { + cJSON_AddNumberToObject(p, "offset_ns", s.offset_ns); + cJSON_AddNumberToObject(p, "freq_ppb", round(s.freq_ppb)); + } + if (s.delays.n) { + win_stats(&s.delays, &mean, NULL, NULL, &sd); + cJSON_AddNumberToObject(p, "path_delay_ns", round(mean)); + cJSON_AddNumberToObject(p, "path_delay_sd_ns", round(sd)); + } + 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); + } + } + xSemaphoreGive(s.lock); +} + esp_err_t ptp_clock_start(esp_netif_t *netif) { s.netif = netif; + s.lock = xSemaphoreCreateMutex(); load_config(); uint8_t mac[6]; esp_netif_get_mac(netif, mac); diff --git a/components/aes67_ptp/ptp_clock.h b/components/aes67_ptp/ptp_clock.h index 76f0f4b..8628aa4 100644 --- a/components/aes67_ptp/ptp_clock.h +++ b/components/aes67_ptp/ptp_clock.h @@ -1,7 +1,10 @@ -// PTPv2 ordinary clock over UDP/IPv4 (E2E). Step 3.2: TimeReceiver measurement only. +// PTPv2 ordinary clock over UDP/IPv4 (E2E). TimeReceiver (TimeTransmitter: step 6). #pragma once +#include "cJSON.h" #include "esp_err.h" #include "esp_netif.h" esp_err_t ptp_clock_start(esp_netif_t *netif); +// Adds the "ptp" object to /api/status. +void ptp_clock_status(cJSON *st);