Step 3.3b: status.ptp for the web UI
- ptp_clock_status() adds status.ptp: state (LISTENING / UNCALIBRATED / SLAVE), locked, gm_id, offset_ns, freq_ppb, path_delay_ns + path_delay_sd_ns (window of 64, from lock on), steps_removed, gm_time/freq_traceable, version, own_class (255 slave / 248 auto, master), clock_id, gm_class/accuracy/p1/p2, sync_avg/min/max_ms and sync_jitter_us (HW Sync intervals), announce_avg_ms, delay_req/resp counters, hw_ts, window. Snapshot under a mutex shared with the PTP task. - Per-Sync log moved to debug; info level logs state changes plus a 60 s summary (max |offset|, freq, delay). - Verified vs ptp4l: LISTENING -> UNCALIBRATED -> SLAVE in ~35 s; locked offset within a few hundred ns, delay 10.27 us +-0.20 us, Delay_Req/Resp 69/69, Sync avg 1000.097 ms, Announce avg 2000 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
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));
|
||||
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;
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user