Skip to content

feat(ethernet/ptp): added support for gPTP (IDFGH-17258)#18249

Open
scrambletools wants to merge 1 commit intoespressif:masterfrom
scrambletools:master
Open

feat(ethernet/ptp): added support for gPTP (IDFGH-17258)#18249
scrambletools wants to merge 1 commit intoespressif:masterfrom
scrambletools:master

Conversation

@scrambletools
Copy link

@scrambletools scrambletools commented Feb 17, 2026

Description

  • Added support in ptpd.c for peer delay requests/responses/followups. Added necessary changes for BMCA, peer delay and other calculations to support gPTP profile
  • Added minor changes to ptpv2.h to support message data needed for gPTP.
  • Added gPTP related settings to menuconfig
  • Motivation is to allow for ESP32P4 to have a fully functional PTP stack with support for gPTP to enable AVB functionality on a low-cost solution

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:

  • 🚨 This PR does not introduce breaking changes.
  • All CI checks (GH Actions) pass.
  • Documentation is updated as needed.
  • Tests are updated or added as necessary.
  • Code is well-commented, especially in complex areas.
  • Git history is clean — commits are squashed to the minimum necessary.

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) to ptpd, 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 of PDELAY_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_MSEC to *_INTERVAL_MS, replaces server delay interval config with NETUTILS_PTPD_DELAYREQ_INTERVAL_MS, adds peer-delay stability tuning), and extends status reporting (ptpd.h) to include peer_delay_ns.

Written by Cursor Bugbot for commit 94f59e5. This will update automatically on new commits. Configure here.

@github-actions
Copy link

github-actions bot commented Feb 17, 2026

Fails
🚫

node failed.

Log

Details
┌─────────┬─────────────────────────────────────┬───────────────────────────────────────────────────────┬──────────────────────────────┐
│ (index) │             CiVariable              │                         Value                         │        CustomSettings        │
├─────────┼─────────────────────────────────────┼───────────────────────────────────────────────────────┼──────────────────────────────┤
│    0    │  'ENABLE_RULE_PR_COMMIT_MESSAGES'   │                         true                          │          'default'           │
│    1    │    'ENABLE_RULE_PR_DESCRIPTION'     │                         true                          │          'default'           │
│    2    │     'ENABLE_RULE_PR_SIZE_LINES'     │                         true                          │          'default'           │
│    3    │ 'ENABLE_RULE_PR_SOURCE_BRANCH_NAME' │                         true                          │          'default'           │
│    4    │   'ENABLE_RULE_PR_TARGET_BRANCH'    │                         true                          │          'default'           │
│    5    │  'ENABLE_RULE_PR_TOO_MANY_COMMITS'  │                         true                          │          'default'           │
│    6    │    'ENABLE_OUTPUT_INSTRUCTIONS'     │                         true                          │          'default'           │
│    7    │             'CLA_LINK'              │     'https://cla-assistant.io/espressif/esp-idf'      │   'custom (default is: )'    │
│    8    │   'COMMIT_MESSAGE_ALLOWED_TYPES'    │ 'change,ci,docs,feat,fix,refactor,remove,revert,test' │          'default'           │
│    9    │      'CONTRIBUTING_GUIDE_FILE'      │                   'CONTRIBUTING.md'                   │   'custom (default is: )'    │
│   10    │   'IGNORED_SECTIONS_DESCRIPTION'    │              'related,release,breaking'               │          'default'           │
│   11    │         'IS_GITLAB_MIRROR'          │                         true                          │ 'custom (default is: false)' │
│   12    │   'MAX_COMMIT_MESSAGE_BODY_LINE'    │                          100                          │          'default'           │
│   13    │    'MAX_COMMIT_MESSAGE_SUMMARY'     │                          72                           │          'default'           │
│   14    │         'MAX_COMMITS_WARN'          │                           5                           │          'default'           │
│   15    │            'MAX_COMMITS'            │                           2                           │          'default'           │
│   16    │           'MAX_PR_LINES'            │                         1000                          │          'default'           │
│   17    │    'MIN_COMMIT_MESSAGE_SUMMARY'     │                          20                           │          'default'           │
│   18    │     'MIN_PR_DESCRIPTION_LENGTH'     │                          50                           │          'default'           │
└─────────┴─────────────────────────────────────┴───────────────────────────────────────────────────────┴──────────────────────────────┘
Error:  RequestError [HttpError]: API rate limit exceeded for 57.151.128.132. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
    at /node_modules/@octokit/request/dist-node/index.js:86:21
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  status: 403,
  response: {
    url: 'https://api.github.com/repos/espressif/esp-idf',
    status: 403,
    headers: {
      'access-control-allow-origin': '*',
      'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Used, X-RateLimit-Resource, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset',
      connection: 'close',
      'content-length': '280',
      'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
      'content-type': 'application/json; charset=utf-8',
      date: 'Tue, 17 Feb 2026 09:31:48 GMT',
      'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin',
      server: 'Varnish',
      'strict-transport-security': 'max-age=31536000; includeSubdomains; preload',
      'x-content-type-options': 'nosniff',
      'x-frame-options': 'deny',
      'x-github-media-type': 'github.v3; format=json',
      'x-github-request-id': 'E442:1EC5C9:4286D30:43F23AC:69943584',
      'x-ratelimit-limit': '60',
      'x-ratelimit-remaining': '0',
      'x-ratelimit-reset': '1771323164',
      'x-ratelimit-resource': 'core',
      'x-ratelimit-used': '60',
      'x-xss-protection': '1; mode=block'
    },
    data: {
      message: "API rate limit exceeded for 57.151.128.132. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
      documentation_url: 'https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting'
    }
  },
  request: {
    method: 'GET',
    url: 'https://api.github.com/repos/espressif/esp-idf',
    headers: {
      accept: 'application/vnd.github.v3+json',
      'user-agent': 'octokit-rest.js/18.12.0 octokit-core.js/3.6.0 Node.js/18.15.0 (linux; x64)'
    },
    request: { hook: [Function: bound bound register] }
  }
}
danger-results://tmp/danger-results-0437aec1.json

Generated by 🚫 dangerJS against 94f59e5

@scrambletools
Copy link
Author

@kostaond I just resubmitted the PR, please let me know when you can look at it.

@github-actions github-actions bot changed the title feat(ethernet/ptp): added support for gPTP feat(ethernet/ptp): added support for gPTP (IDFGH-17258) Feb 17, 2026
@espressif-bot espressif-bot added the Status: Opened Issue is new label Feb 17, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

(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 */
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

default 100
help
Sends peer delay request only once the internal clock is stable and skews only
in defined interval.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

static uint16_t log_period_to_msec(int8_t log_period) {
// interval = 2^logMessagePeriod
return (uint16_t)(pow(2.0, log_period) * 1e3);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

ptp_gettime(state, &ts);
#endif // !ESP_PTP
timespec_to_ptp_format(&ts, msg.origintimestamp);
timespec_to_ptp_format(&ts, msg.follow_up.origintimestamp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web


/* Randomize up to 2x nominal delay) */
state->peer_delay_ns +=
(peer_delay - state->peer_delay_ns) / state->peer_delay_avgcount;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

@kostaond
Copy link
Collaborator

@scrambletools thank you for the update. Our ptp daemon is based on Nuttx implementation. I've just noticed that there was an update recently. I would like to keep track with the Nuttx upstream version. It unfortunately means another delay for your PR :(

I understand that this waiting must be very frustrating for you. Let me propose a compromise.

I can extract ptp into a separate component and publish it via the Espressif Component Registry — this is planned anyway. You could then fork it and distribute your solution under your own namespace in the registry. Once things settle, we can merge your fork back upstream.

Would this approach work for you? If so, I should be able to prepare the standalone ptp component within a week or two.

@scrambletools
Copy link
Author

@kostaond that sounds ok. I have a few questions if you don't mind:
How far behind is it from Nuttx master?
What is the cadence for sync'ing with Nuttx?
Some features are gated behind ESP-specific configs. How will those features be enabled for Nuttx?
Once ptp is moved to the component registry, it will be maintained there (not in the example tree) correct?

@scrambletools
Copy link
Author

@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.

@kostaond
Copy link
Collaborator

I have a few questions if you don't mind:

  • TBH, I ported it in rush at the time so I did't note specifically but I think ESP-IDF version is at 86f3671 of Nuttx
  • As you can see in the history, there were no functional updates in Nuttx for almost 2 years
  • I'm not sure if I fully understand to the question. However, I'm planning to migrate ESP-IDF version to https://github.com/espressif/esp-protocols/tree/master/components repo as it is. Hence the daemon should stay more or less the same.
  • The pdp daemon component will be maintained at https://github.com/espressif/esp-protocols/tree/master/components repo and distributed over Component registry. Opened question is if to keep the ptp example in the IDF which would be dependent on ptp component or migrate the example to Protocols repo, or combination of both. ptpd will definitely be separate component, the question is what to do with esp_eth_time(that was probably your question, right?). Ideally, it would be better to be part of IDF. I'll start like that and I will see if it's possible.

@kostaond
Copy link
Collaborator

kostaond commented Feb 18, 2026

I plan to support gPTP over wifi in a future update

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.

@scrambletools
Copy link
Author

@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.

@kostaond
Copy link
Collaborator

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);
Copy link
Contributor

@safocl safocl Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 issues

J.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;
}

Copy link
Contributor

@safocl safocl Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

@scrambletools
Copy link
Author

@kostaond do you have an estimate when ptpd will move to https://github.com/espressif/esp-protocols/tree/master/components?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Opened Issue is new

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants