Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e763dc81bd | |||
| 5bd1a20e43 | |||
| dcea39b216 | |||
| 08b83a069c | |||
| 6d85eb49f8 |
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user