feat(ethernet/ptp): added support for gPTP (IDFGH-17258)#18249
feat(ethernet/ptp): added support for gPTP (IDFGH-17258)#18249scrambletools wants to merge 1 commit intoespressif:masterfrom
Conversation
LogDetails |
|
@kostaond I just resubmitted the PR, please let me know when you can look at it. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 7 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
This is the final PR Bugbot will review for you during this billing cycle
Your free Bugbot reviews will reset on February 20
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| || memcmp(a->gm_identity, b->gm_identity, sizeof(a->gm_identity)) == 0) | ||
| { | ||
| system_identity_check = 0; | ||
| } |
There was a problem hiding this comment.
BMCA equality check uses OR instead of AND
High Severity
The else if condition in is_better_clock uses || (OR) operators for the equality check when it needs && (AND). This causes system_identity_check to be set to 0 (equal) whenever any single field matches between two clocks, rather than requiring all fields to match. In practice, two clocks with very different priorities or quality will be treated as having equal system identities if even one byte happens to coincide, causing the BMCA to skip the proper hierarchical comparison and fall through to stepsremoved comparison incorrectly.
| (memcmp(a->header.sourceidentity, b->header.sourceidentity, | ||
| sizeof(a->header.sourceidentity)) == 0) && | ||
| (memcmp(a->header.sourceportindex, b->header.sourceportindex, | ||
| sizeof(a->header.sourceportindex)) < 0))) /* Compare port number */ |
There was a problem hiding this comment.
BMCA cascading comparison uses wrong stepsremoved operator
High Severity
The third and fourth conditions in the BMCA comparison at is_better_clock require stepsremoved < 0 when they need stepsremoved == 0 (equal). Since the second condition already handles stepsremoved < 0, the third and fourth conditions are unreachable — they can never add a true result that the second condition didn't already catch. This means when stepsremoved are equal, sourceidentity and sourceportindex tiebreakers are never evaluated, causing incorrect BMCA results.
| pathtrace_tlv.length[1] = 8; // 8 bytes | ||
| memcpy(pathtrace_tlv.pathsequence, state->own_identity.gm_identity, | ||
| sizeof(state->own_identity.gm_identity)); | ||
| msg.pathtracetlv = pathtrace_tlv; |
There was a problem hiding this comment.
Uninitialized pathtrace TLV type and length bytes
Medium Severity
The stack-allocated pathtrace_tlv struct is not zero-initialized before use. Only type[1], length[1], and pathsequence are assigned — type[0] and length[0] remain uninitialized and will contain garbage values. The TLV type for path trace is 0x0008, which requires type[0] to be 0, and the length 0x0008 requires length[0] to be 0. Announce messages will have a corrupted path trace TLV.
| default 100 | ||
| help | ||
| Sends peer delay request only once the internal clock is stable and skews only | ||
| in defined interval. |
There was a problem hiding this comment.
Kconfig and code have mismatched config name
Medium Severity
The Kconfig defines NETUTILS_PTPD_PEER_PDELAY_STABILITY_NS (with "PDELAY"), but the code references CONFIG_NETUTILS_PTPD_PEER_DELAY_STABILITY_NS (with "DELAY", no extra P). This naming mismatch means the user's Kconfig setting is never picked up by the code, and the #ifndef fallback default of 100 is always used regardless of what's configured.
Additional Locations (1)
| static uint16_t log_period_to_msec(int8_t log_period) { | ||
| // interval = 2^logMessagePeriod | ||
| return (uint16_t)(pow(2.0, log_period) * 1e3); | ||
| } |
There was a problem hiding this comment.
log_period_to_msec overflows uint16_t for large periods
Medium Severity
log_period_to_msec returns uint16_t (max 65535), but the caller passes logmessageinterval values up to 12, where pow(2, 12) * 1000 = 4,096,000 — far exceeding uint16_t range. For values 7 and above, the result silently wraps, producing an incorrect delayreq_interval_ms. The return type needs to be wider (e.g., uint32_t) to match the long type of delayreq_interval_ms.
Additional Locations (1)
| ptp_gettime(state, &ts); | ||
| #endif // !ESP_PTP | ||
| timespec_to_ptp_format(&ts, msg.origintimestamp); | ||
| timespec_to_ptp_format(&ts, msg.follow_up.origintimestamp); |
There was a problem hiding this comment.
Follow-up messages lose gPTP SDOID flag
Medium Severity
When constructing follow-up messages in gPTP mode, the messagetype is assigned with a plain = (e.g., PTP_MSGTYPE_FOLLOW_UP or PTP_MSGTYPE_PDELAY_RESP_FOLLOW_UP), overwriting the PTP_MSGTYPE_SDOID_GPTP flag that was OR'd into the preceding sync/response message. The gPTP majorSdoId (upper nibble) is lost, so follow-up messages are sent as standard PTP rather than gPTP. Other conformant gPTP implementations may reject or ignore these messages.
Additional Locations (1)
|
|
||
| /* Randomize up to 2x nominal delay) */ | ||
| state->peer_delay_ns += | ||
| (peer_delay - state->peer_delay_ns) / state->peer_delay_avgcount; |
There was a problem hiding this comment.
Division by zero in peer delay averaging
High Severity
When gPTP is enabled without CONFIG_NETUTILS_PTPD_SEND_DELAYREQ, CONFIG_NETUTILS_PTPD_DELAYREQ_AVGCOUNT falls back to the #ifndef default of 0. In ptp_process_delay_resp_follow_up, peer_delay_avgcount starts at 0 and is never incremented (since 0 < 0 is false), causing a division by zero on the very first peer delay measurement. This is reachable with default Kconfig settings (CLIENT=y, SEND_DELAYREQ=n) plus gPTP enabled.
Additional Locations (1)
|
@scrambletools thank you for the update. Our I understand that this waiting must be very frustrating for you. Let me propose a compromise. I can extract Would this approach work for you? If so, I should be able to prepare the standalone |
|
@kostaond that sounds ok. I have a few questions if you don't mind: |
|
@kostaond I plan to support gPTP over wifi in a future update (using FTM to calc peer delay) so you may want to maintain ptp somewhere that is not specific to ethernet. |
|
That would be super neat! We discussed it too, but we currently don't have resources. Anyway, having ptp as separate component should make your life easier with distribution of your solution to the public. |
|
@kostaond regarding where you plan to maintain ptpd, that makes sense. My other question was regarding how to keep the features of ptpd working in both nuttx and esp-idf. I am not testing for nuttx, only esp-idf. |
|
I see, we are not planning to merge to Nuttx upstream. We will maintain just our version and time to time sync with Nuttx. |
| if (!state->selected_source_valid) { | ||
| return OK; | ||
| } // ignore if operating as a server in gPTP profile | ||
| return ptp_process_sync(state, &state->rxbuf.sync); |
There was a problem hiding this comment.
C draft "ISO/IEC 9899:TC2 Committee Draft — May 6, 2005 WG14/N1124":
6.5.2.3 Structure and union members
[...]
5 One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the complete type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
8 The following is not a valid fragment (because the union type is not visible within function f):
struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 *p1, struct t2 *p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
Annex J
(informative)
Portability issuesJ.1 Unspecified behavior
The following are unspecified:
[...]
— The value of a union member other than the last one stored into (6.2.6.1).
6.2.6 Representations of types
6.2.6.1 General
[...]
7 When a value is stored in a member of an object of union type, the bytes of the object
representation that do not correspond to that member but do correspond to other members
take unspecified values.
(in C++ this is undefined behavior).
You can simply wrap it in a separate structure struct ptp_header_s header; (but accordingly change access everywhere through an additional union member):
typedef union
{
struct { struct ptp_header_s header; } general;
struct ptp_announce_s announce;
struct ptp_sync_s sync;
struct ptp_follow_up_s follow_up;
struct ptp_delay_req_s delay_req;
struct ptp_delay_resp_s delay_resp;
struct ptp_delay_resp_follow_up_s delay_resp_follow_up;
uint8_t raw[128];
} ptp_msgbuf;
ptp_msgbuf u = {.sync = {}};
if (u.general.header.messagetype == MAGIC_NUMBER_FOR_SYNC){
struct ptp_sync_s s1 = u.sync;
} else if (u.general.header.messagetype == MAGIC_NUMBER_FOR_FOLLOW){
struct ptp_follow_up_s s1 = u.follow_up;
}There was a problem hiding this comment.
6.7.2.1 Structure and union specifiers
13 Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
14 The size of a union is sufficient to contain the largest of its members. The value of at most one of the members can be stored in a union object at any time. A pointer to a union object, suitably converted, points to each of its members (or if a member is a bit-field, then to the unit in which it resides), and vice versa.
I can't find any provisions here about the possibility of converting a pointer to one type A into a pointer to another type B when there is a third type C that can be pointer-interconvertible (suitably converted) to B when type B is nested in type A, and then C can be pointer-interconvertible (suitably converted) to A.
as in C++ standard:
https://eel.is/c++draft/basic.compound#5
5 Two objects a and b are pointer-interconvertible if
[...]
(5.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
then you could simply convert the pointer to the union into a header type:
ptp_msgbuf u = {.sync = {}};
if (((ptp_header_s*)u)->messagetype == MAGIC_NUMBER_FOR_SYNC){
struct ptp_sync_s s1 = u.sync;
} else if (u.general.header == MAGIC_NUMBER_FOR_FOLLOW){
struct ptp_follow_up_s s1 = u.follow_up;
}|
@kostaond do you have an estimate when ptpd will move to https://github.com/espressif/esp-protocols/tree/master/components? |


Description
Related
This is a re-submission of a previous PR that is defunct (#15001 reviewed by @kostaond) due to an unrecoverable error in rebasing
Testing
Tested on esp32p4 rev0.1 using 2 Waveshare ESP32 P4 Nanos connected directly to each other.
Enable/disabled gPTP via menuconfig and tested with different menuconfig settings.
Checklist
Before submitting a Pull Request, please ensure the following:
Note
Medium Risk
Touches core PTP synchronization logic (BMCA selection, packet formats, delay calculation, and clock adjustment), which can affect time accuracy and interoperability across networks, though it is gated behind a new gPTP config option.
Overview
Adds an optional gPTP profile mode (
CONFIG_NETUTILS_PTPD_GPTP_PROFILE) toptpd, switching it to 802.1AS behavior (LLDP multicast destination, gPTP flags/SDO ID, mandatory two-step sync, and updated BMCA comparisons).Implements peer-delay (PDelay) support end-to-end: new message types/structs and TLVs in
ptpv2.h, new state tracking (peer_delay_ns/correction field), handling ofPDELAY_REQ/PDELAY_RESP/PDELAY_RESP_FOLLOW_UP, and updated clock-correction math to use peer delay + correction field when in gPTP.Updates Kconfig to expose gPTP and timing knobs (renames
*_INTERVAL_MSECto*_INTERVAL_MS, replaces server delay interval config withNETUTILS_PTPD_DELAYREQ_INTERVAL_MS, adds peer-delay stability tuning), and extends status reporting (ptpd.h) to includepeer_delay_ns.Written by Cursor Bugbot for commit 94f59e5. This will update automatically on new commits. Configure here.