Files
aes67-ESP32-P4/CLAUDE.md
T
bsncubed 5bd1a20e43 Step 6.3: PTP hybrid mode (unicast Delay_Req/Resp), step 6 done
- As TimeReceiver in ptp.mode hybrid: Delay_Req unicast to the GM (IP
  from its Announce, MAC recorded by the RX hook from its Sync frames),
  unicastFlag set.
- As TimeTransmitter: a Delay_Req with the unicastFlag gets a unicast
  Delay_Resp; multicast requests get multicast replies.
- EMAC timestamps every received frame (en_ts4all): its PTP filter left
  unicast Delay_Req unstamped ("no HW RX timestamp").
- A unicast Delay_Resp's logMessageInterval 0x7F is ignored (use
  ptp.log_delay_req); log_us() clamps to -7..6 (the shift was undefined).
- Verified vs ptp4l --hybrid_e2e 1: board hybrid TimeReceiver 18
  Delay_Req in 20 s, all answered, locked; board TimeTransmitter
  (p1 100): tcpdump shows Sync/Follow_Up/Announce to 224.0.1.129,
  Delay_Req 192.168.192.233 -> .244 [unicast] and Delay_Resp .244 ->
  .233 [unicast] within 0.4 ms; ptp4l s2.
- Docs: TimeTransmitter/hybrid notes; step 6 ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:21:09 +10:00

5.0 KiB

aes67-ESP32-P4

AES67 sender on a Waveshare ESP32-P4-ETH (PoE). Sources: Spotify Connect (cspot) and HLS/m3u8, with failover. Riedel-style web UI and a reusable AES67 core (PTP, RTP TX, SDP/SAP, VLAN, syslog, health) meant for later AES67 projects.

Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4

Read first

  • docs/aes67-core-base.md: reusable core. Config schema, API, PTP (roles, Riedel defaults, status fields), AES67 TX/SDP/SAP, VLAN, syslog, temperatures, component layout.
  • docs/hardware-and-design-notes.md: board pinout, chip revision caveat, project pipeline (cspot/HLS), failover, player API.
  • web/index.html: finished web UI (single file). It is the API contract; firmware must match the JSON it reads and writes.

Working rules

  • One change at a time ("one fuckup at a time"). Change or test one variable per step; never stack several fixes or hypotheses. If something breaks, go back to the last known-good state.
  • Follow the build order below. Finish and verify a step on hardware before starting the next.
  • After every change: idf.py build, flash, and check the serial monitor output. Show the relevant log lines.
  • Don't mark a step done until it has been verified on the board.
  • Keep core and project separate: components/aes67_* must not depend on main/ (sources/player). Project code plugs in through the config, status and route registries.
  • web/index.html: keep the CORE / PROJECT markers. If the firmware needs an API change, change the doc and the page together.
  • Small, focused commits per step.

Toolchain

  • ESP-IDF 5.5 or later (needed for the P4 and for examples/network/vlan_support). Target esp32p4.
  • idf.py set-target esp32p4, idf.py build, idf.py -p <PORT> flash monitor
  • Before first build: run esptool.py chip_id and esptool.py flash_id.
    • Chip revision < v3.0 (engineering silicon, seen on some of these boards) needs CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y. Our board: v1.3 (set in sdkconfig.defaults, min rev v1.0).
    • Our board: 32 MB flash (GigaDevice c8/4019). App slots must stay below 16 MB (cache mapping above 16 MB is experimental in IDF).
  • Embed web/index.html via EMBED_TXTFILES in aes67_web.
  • Flash over the network (normal way since step 2b; keep USB for recovery): curl -f --data-binary @build/aes67_p4.bin -H 'Content-Type: application/octet-stream' http://p4-aes67/api/ota The board reboots into the new image on trial; check GET /api/ota shows the new version with pending_verify: false.
  • Serial: opening /dev/ttyACM0 can reset the board. Don't open it while an OTA image is on trial (a reset then counts as a failed boot and rolls back).
  • Rollback test build (self-test always fails), in its own build dir: idf.py -B build-selftest-fail -DSDKCONFIG=build-selftest-fail/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.selftest_fail" build

Board quick reference (full details in docs)

  • Ethernet: IP101GRI, RMII, PHY addr 1, ref clock from PHY into GPIO50 (EMAC_CLK_EXT_IN). MDC 31, MDIO 52, PHY reset/power 51, TX_EN 49, TXD0 34, TXD1 35, CRS_DV 28, RXD0 29, RXD1 30.
  • No Wi-Fi/BT on this board.
  • On-die temperature sensor only (driver/temperature_sensor.h).

Build order / status

  • 0. Check chip revision and flash size; create IDF project and empty component stubs (layout in aes67-core-base.md). Set up the OTA partition table (two app slots, no factory) and PROJECT_VER from git now, so the layout never changes later.
  • 1. Ethernet: IP101 up, DHCP, IP logged. Ping works.
  • 2. Web server + config store (cJSON in NVS) + embedded index.html; /api/config, /api/status (stub values), /api/reboot.
  • 2a. Finding the device: mDNS (hostname.local + _http._tcp), then LLDP (switch shows name + IP).
  • 2b. Firmware update: /api/ota upload + rollback self-test. Test: update to a new build, then deliberately flash a build that fails its self-test and confirm it rolls back. After this, flash over the network; keep USB for recovery.
  • 3. PTP TimeReceiver: lock to an existing GM (Riedel), fill status.ptp. Confirm EMAC hardware timestamps work. First check whether the installed ESP-IDF has a PTP example/component for the P4 before writing one.
  • 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.
  • 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.
  • 7. Sources: HLS player, then cspot (Spotify Connect), then failover + /api/player.
  • 8. Mono sum, gain, polish.

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.