Save/Revert/Reboot and the status message stay at the bottom of the window
while the settings form is in view. The runtime source override stays in
the API (listed in the page's API box) but is no longer on the page, where
it duplicated the saved source mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The player now owns the effective mode (config or override) and passes it
down: spotify_apply(enable) and hls_set_url(url) instead of both reading
source.mode from the config. /api/player/source forces spotify|hls|off
(config returns to the saved mode), /api/player/url plays an m3u8 now;
neither is saved. GET reports it in `forced`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows /api/player's position, moves on locally between polls while
playing, seeks on release; disabled when the source can't seek.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seek {ms} runs on the Spotify session task like a seek from the app (buffer
flushed, app notified). volume {value}|{delta} sets the player volume and
pushes it to the session, so the app's slider follows; setRemoteVolume now
also runs on the session task.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
play/pause/toggle/stop/next/prev. Spotify commands run on the session task
(same thread as cspot's frame handling) and the POST waits until they ran,
so it answers the new state. HLS pause stops fetching and resume rejoins at
the live edge; next/prev on HLS and anything on tone/off return 409.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Source, state, artist/title/album, position and duration (from cspot's
playback state, so it matches the app), volume and `can`. cspot patch 0004
adds read getters for the playback state and context.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No Spotify session (or paused, with failover_on_pause) for failover_delay_s
switches to HLS; Spotify playing switches back within ~1 s. Switches fade
out/in over 30 ms and flush the ring. HLS is suspended while Spotify plays.
cspot is held back instead of drained while it isn't Spotify's turn: it keeps
decoding on pause, so dropping its frames let it race through the queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mac dropped the device soon after the first track change: we never
called SpircHandler::notifyAudioReachedPlayback(), so cspot's queue did
not advance and the state sent to Spotify stalled.
- The data callback records a boundary (output write position, track
id) when the track id changes; the session loop (<= 200 ms) calls
notifyAudioReachedPlayback(id) once playback (ring read position)
passes it, and notifyAudioEnded() after DEPLETED once the buffer is
empty. PLAYBACK_START clears boundaries (next data is a new one),
seek/flush drop pending ones.
- audio_ring exposes its write/read frame counters (wrap-safe compare).
- Verified: 10 min on the Mac without a disconnect, every track change
notified ("track audible, Spotify notified"), app shows the playing
track (switches up to ~3-5 s early), 0 underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Spotify AP reset the connection (recv -1, errno 104 ECONNRESET) on
the third Ping, every 6 minutes, like clockwork. cspot answered each
Ping at once; librespot (core/src/session.rs) waits 60 s:
Ping -> 60 s -> Pong -> PongAck -> 60 s -> Ping.
- 0003-delayed-pong: record the Ping, send the Pong 60 s later from
triggerTimeout() (called on every 3 s receive timeout), through the
PR #3 connection snapshot; dropped on reconnect.
- 0002-diag-log-recv-errors: log recv's return value/errno before
"Error in read" (error path only); this is what showed the RST.
Verified: over 8+ minutes Ping/delayed Pong/PongAck every 2 min, no
reset at the 6-minute mark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MercurySession::reconnect() nulls conn/shanConn (for 5 s per failed
retry) while other tasks keep sending through them: a NULL dereference,
not a catchable exception. Our Spotify sessions are dropped by the
server and reconnect every ~6 minutes, so this race is hit regularly;
likely behind the sporadic resets during long sessions/OTA.
Patch applies cleanly to the pinned 3010349; builds, boots, confirmed.
Drop it once the fix is in the pinned cspot commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- OTA: the player now ends the Spotify session (spotify_suspend: stop
flag, waits until the session's tasks are gone, refuses new ones)
instead of pausing it, and suspends HLS. Pausing was not enough (an
upload still failed once with a paused session). Verified twice with a
session running for minutes: "Disconnecting mercury session / session
ended / suspended", installed in ~26 s, clean reboot.
- Volume: createFromBlob() starts cspot at volume 0, which the app
showed while we played at 100 %. The session now starts with the
player's volume (spotify_set_volume); volumes coming from the app are
stored exactly, without echo. Verified: slider starts at 100 %.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spotify_init() sets up the logger, zeroconf routes and session task
once; spotify_apply() (at boot and on every source save) enables or
disables: enabled = mode spotify/auto + credentials. Disable removes
the mDNS entry, zeroconf answers 404 and a running session ends
(loop exits within 200 ms, SpircHandler::disconnect stops the queue
and player tasks). A new device name ends the session and
re-advertises. The login blob is swapped under a mutex.
- Verified: hls/spotify/rename switch the mDNS entry and /spotify_info
(404/200) live; switching to hls during Spotify playback ended the
session in < 2 s, HLS played after ~5 s, internal heap 221 -> 365 KB
(cspot frees everything).
- CLAUDE.md: OTA-with-session findings (intermittent, flash-write stalls
seen as TX resyncs during uploads), serial-port reset caveat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upload during an active Spotify session crawled (~10 kB/s, 197 s)
and ended in a reset (seen twice). Workaround:
- aes67_ota_on_update(cb): cb(true) right before an accepted upload
writes flash, cb(false) if it then fails.
- The player pauses Spotify (spotify_pause -> SpircHandler::setPause, the
app follows) and suspends HLS fetching (hls_suspend); resumed if the
upload fails. AES67 TX keeps running (silence).
- Verified: upload during Spotify playback paused the session and
installed in 25.8 s (normal for 2 MB), clean reboot and self-test.
Root cause still open (noted in CLAUDE.md with the other cspot items).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- player: volume 0..100 %, amplitude = (v/100)^3 (about -18 dB at 50 %,
0 = mute), applied in the TX pull callback after the ring so a change
is heard at once (the ring holds 4 s); ramped over one packet to avoid
zipper noise. Spotify VOLUME events (0..65535) set it.
- Docs: pipeline order ring -> volume/gain, curve described.
- Verified from a Mac: RMS followed the slider immediately (100 % about
-10 dBFS; steps to about -24.5 and -28 dBFS match the cubic curve for
~57 % / ~50 %); user: "sounds about right". Mute at 0 % not yet
confirmed (check via /api/player/volume later).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spotify_init(pcm_cb, event_cb): cspot's data callback hands 44.1 kHz
stereo s16 PCM to the player and returns what was taken; the player's
blocking ring write paces decoding to playback speed.
- Events: FLUSH/SEEK/PLAYBACK_START empty the ring (skip/seek sound at
once); PLAY_PAUSE pauses output (silence, buffer kept, no underrun);
VOLUME logged (applied in b4).
- player: Spotify via its own audio_out converter (44.1 -> 48 kHz);
source_state playing / paused / buffering.
- Verified from a Mac: music on the AES67 stream (10 s: 0 gaps, no
silent packets, RMS -9.6 dBFS, peak 0.0 dBFS), buffer 4.0 s full,
0 underruns, pause/play/skip/seek events.
Seen: first 3 connects "Can't connect to spotify servers", 4th worked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/audio_out: one converter instance per source (own rate converter
state): s16 any rate/channels -> 48 kHz stereo int32 -> ring.
- player_write(src, ...) only writes for the active source, drops the
rest; player_src_t is public in player.h.
- HLS decoder uses audio_out (no behaviour change).
- Verified: HLS still sample exact (441344 -> 480375 frames per
10.008 s segment), buffer ~4 s, 0 underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Zeroconf on our httpd (GET/POST /spotify_info, form body URL-decoded)
and _spotify-connect._tcp via our mDNS (VERSION, CPath, Stack TXT).
- Session task: waits for the app's login blob, connects with the
user's client ID/secret and the configured bitrate (96/160/320 ->
OGG_VORBIS_*), SpircHandler, events logged; cspot exceptions caught.
Starts when source.mode is spotify/auto and credentials are set.
- bell::bellGlobalLogger was NULL: every CSPOT_LOG crashed the board
right after addUser (Load access fault in Session.cpp:67 /
LoginBlob.cpp:58). cspot/bell logs now go to esp_log (UART + syslog);
signed CDN URL tokens are cut from the log.
- esp_audio_codec's own Vorbis decoder clashed with bell's Tremor
symbols: CONFIG_AUDIO_DECODER_VORBIS_SUPPORT / SIMPLE_DEC_OGG off.
- status.spotify_state from the session (waiting / connecting /
connected / login failed / error / disabled / no client credentials).
- Verified from a Mac: device appears, "connected as <user>", access
token fetched with the user's client credentials, track info, audio
key, CDN URL (the step broken upstream), PCM decoded. Without
backpressure a track decodes in ~26 s (b3 feeds the ring).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Core config: cfg_mark_secret(group, key). GET /api/config returns ""
for secret keys; a POST with "" keeps the stored value, null clears it;
cfg_get() returns the real value. Documented in aes67-core-base.md.
- source.spotify_client_id / spotify_client_secret (secret write-only):
each user's own Spotify developer app, needed by the maintained cspot
fork (philippe44/cspot) since Spotify's 2025 API restrictions.
- UI: client ID field, password field for the secret ("leave empty to
keep"), link to developer.spotify.com; enabled for spotify/auto modes.
- status.spotify_state: "no client credentials" while either is missing.
- Verified: secret never appears in GET; UI-style re-save keeps it;
survives reboot; null clears it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HLS plays on AES67; list what to come back to (clock drift vs PTP,
download speed, untested cases) before moving on to cspot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decoder: s16 PCM -> stereo int32 -> esp_ae_rate_cvt 44.1 -> 48 kHz
(32-bit, complexity 3; bypassed at 48 kHz; mono duplicated) -> ring.
esp_audio_effects pinned to ~1.3.0 (1.4+ needs P4 rev >= 3).
- hls: each segment is downloaded completely into PSRAM (max 4 MB), then
decoded; the connection is not held open while the decoder waits for
ring space at playback speed.
- player: player_write() blocks while the ring is full and gives up when
the source changes; the temporary 440 Hz producer is removed.
- Verified with Triple J Hottest: 441344 -> 480375 frames (10.008 s) and
440320 -> 479260 (9.985 s) per segment; RTP 15000 packets, 0 gaps,
peak -10 dBFS / RMS -22 dBFS; ring ~4.0 s, 0 underruns over ~50 s;
heap 422 KB, PSRAM 27.5 MB free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/decoder: MPEG-TS demux (packets reassembled across HTTP chunks,
PAT -> PMT -> first ADTS-AAC PID, PES headers stripped) feeding the
esp_audio_codec simple AAC decoder (ADTS, AAC-Plus enabled for HE-AAC
v1/v2 variants). 7.3 only counts and logs PCM per segment.
- esp_audio_codec pinned to ~2.5.0: 2.6+ needs P4 rev >= 3 (this board
is rev 1.3). Noted in CLAUDE.md, also for esp_audio_effects < 1.4.
- The library's combined TS decoder lost ~8% of the frames (segments
decoded to 7.9-9.6 s, "decode error -1"); with the own demux every
segment is sample exact: 441344 / 440320 frames = 10.008 / 9.985 s,
matching EXTINF 10.0078 / 9.9846 (431 / 430 AAC frames), no errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/hls: task active while source.mode is hls/auto and hls_url is
set. esp_http_client over HTTPS (IDF certificate bundle), redirects
followed (max 5), streamed reads in 4 KB chunks to a sink callback.
- Master playlist: highest BANDWIDTH variant; URL resolution for
absolute, host-relative and path-relative references; CRLF tolerant.
- Media playlist: TARGETDURATION, MEDIA-SEQUENCE, segments; start 3
segments behind the live edge, fetch each new one in order, skip ahead
if the window moved past us, reload after target/2 when nothing is new.
- Verified with Triple J Hottest (ABC, Akamai): 252 kbit/s AAC-LC
variant chosen, 3 back-fill segments then one new ~10 s segment at a
time, ~303 KB in ~1.25 s each; heap 439 KB, PSRAM 31.9 MB free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main/audio_ring: SPSC ring of interleaved int32 frames in PSRAM
(4 s at 48 kHz stereo), lock-free with acquire/release counters; the
TX pull callback never blocks.
- main/player: source selection from source.mode and the pull callback
for aes67_tx. tone -> the core's PTP-phased 1 kHz tone; off -> silence;
hls/spotify -> ring with 1 s prefill (silence while buffering is not an
underrun; running dry is, and prefills again). Status fields
active_source, source_state, spotify_state, buffer_ms. source config
applies live.
- New source.mode "tone" (validation, UI dropdown, doc) for commissioning.
- Temporary 440 Hz producer in hls mode (until the HLS player exists).
- Verified: tone 999.7 Hz -18 dBFS; off silent; hls 440.0 Hz with max
sample step 60816 (ideal sine 60825, i.e. no discontinuities), buffer
3983 ms, 0 underruns; spotify silent/not implemented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No official Tailscale client exists for ESP32: evaluate a Tailscale
subnet router on the LAN (no firmware change) or WireGuard on the device.
API authentication is a prerequisite before remote exposure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Planned SNTP client: servers as names or IPs, several allowed, resolved
via DNS. Seeds the PTP clock with real time before becoming GM (today it
starts at 1970) and gives syslog timestamps. Added to the phase 2 list
in CLAUDE.md and as a section in aes67-core-base.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- aes67_cfg: cfg_set_number(group, key, value, apply) for firmware-side
changes, stored in NVS like a POST; apply can be skipped.
- aes67_sdp_sap: once a second, a change of the PTP GM (ts-refclk) bumps
aes67.session_ver without re-applying the aes67 group (no stream
restart); SAP then re-announces. Runs whether or not SAP is on.
- Verified with ptp4l as a normal clock (p1 128): board auto (p1 250)
steps down to SLAVE (session_ver 1->2); role master p1 100 via API ->
board MASTER at once (->3); back to auto p1 250 -> ptp4l takes over
within ~1 s, board SLAVE again (->4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Own dataset from config: slave -> class 255, never TimeTransmitter;
auto/master -> class 248 with ptp.priority1/2; accuracy 0xFE, variance
0xFFFF, timeSource 0xA0, clockIdentity EUI-64 from MAC.
- BMCA: a foreign TimeTransmitter is followed only if its Announce beats
our dataset (always for slave-only); if the followed one turns worse we
take over; a better one makes us step down. LISTENING -> MASTER after
announceReceiptTimeout x our announce interval.
- MASTER: Announce (PTP timescale flag) every 2^log_announce, two-step
Sync every 2^log_sync (raw frame, HW TX time in Follow_Up), Delay_Resp
for each Delay_Req with its HW RX time and logMessageInterval =
log_delay_req. Frequency correction kept (holdover).
- ptp config applies live (role, priorities, intervals, domain, DSCP).
- status.ptp: state MASTER, gm_id = own, TX Sync/Announce intervals,
Delay_Req/Resp counters; locked is true as master so AES67 TX keeps
running; SDP ts-refclk and SAP follow.
- Verified: with ptp4l GM p1 128 the board (auto, p1 250) stays SLAVE;
ptp4l stopped -> board MASTER; ptp4l -s --priority1 255 locks to it
(s2) in ~4 s, offset within +-320 ns, path delay 10.3 us.
Known: Sync send times jitter ~10 ms (FreeRTOS tick); accuracy is not
affected (two-step Follow_Up carries the exact HW time).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default 10, PTP (2), AES67 TX, SAP, syslog and httpd's
listen/control sockets left ~3 for web clients although httpd allows 7.
A browser's keep-alive connections then made new requests fail with a
reset for seconds at a time (the intermittent 'outages').
Reproduced: with 1 idle connection held, new requests were reset.
After: 6 idle connections held, new requests answered in 6-7 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SAP, syslog and health/temperatures are done and verified. The VLAN
split needs a tagged VLAN with DHCP on the switch and is only needed
for the internet sources, so it moves to a phase 2 section. Step 4
notes what is still open (Riedel import, Wireshark).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_health: status.temps = [{name, c, min_c, max_c, warn_c}]
(0.1 °C), sampled every 2 s by a health task. On-die "SoC" sensor via
driver/temperature_sensor.h, range 20..100 °C, warn at 85 °C.
health_temp_register(name, read_cb, warn_c) for board sensors.
Warning logged when crossing warn_c, cleared below warn_c - 5 °C.
- Verified: SoC 26-28 °C at idle with min/max since boot; with a
temporary 27 °C threshold the warning was logged and arrived via
syslog as <132>, status carried warn_c 27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
httpd_txrx warned on every client reset (errno 104) and httpd_uri on
every 404 (the UI polls /api/player every 2 s until step 7), flooding
syslog. Both set to error level; our handlers log the 4xx that matter.
Remaining known line: 'httpd_resp_send_err: error calling setsockopt :
22' (error level) when a client closes right after a 4xx; a harmless
race in IDF's CONFIG_HTTPD_ERR_RESP_NO_DELAY handling, the response is
already delivered.
Verified via syslog: 10 status + 10 /api/player requests, no httpd lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a clock step, status.ptp showed the whole step (~1.79e18 ns) as
offset_ns until the next Sync, and the 60 s summary reported it as
max |offset|. Offset is reset to 0 and the summary restarts on a step.
Verified: status goes 0 -> pull-in values; first summary max 24960 ns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_syslog: esp_log_set_vprintf hook chained to the UART; formats
into a static buffer under a zero-timeout mutex (drop and count when
busy), strips ANSI codes, parses level/tag, filters by log.level and
queues (48 messages, non-blocking). Low-priority sender task: RFC 5424
(<PRI>1 - HOST APP - - - MSG, no timestamp yet) or RFC 3164
(<PRI>HOST TAG: MSG), PRI = facility*8 + severity; log.host as IP or
resolved name; config applies live. Messages wait in the queue until
the interface has an IP. POST /api/log/test sends an info message
regardless of the level filter.
- Initialised first in app_main so boot messages are captured; the UDP
socket is created only once the interface is up (creating it before
esp_netif_init crashed at boot and the bootloader rolled back).
- Verified with a UDP listener: 5424 and 3164 formats, PRI 134/132,
boot messages delivered after the IP came up, shutdown messages sent
before reboot, level filter, test message, host by DNS name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_sdp_sap: SAP (RFC 2974) announcer task. SAPv1 to
239.255.255.255:9875 every 30 s and within 1 s of any SDP change
(config save, GM change), with the old hash deleted first. Active while
aes67.discovery = sap, the stream is enabled, the AES67 interface has
an IP and a PTP GM is known. Deletion when that stops and at shutdown
(reboot/OTA) via a shutdown handler. TTL = aes67.ttl.
- Docs: SAP implementation notes; session_ver bump on GM change still open.
- Verified with a SAP listener: 30 s repeats with a stable hash; rename
-> delete old + announce new (o= version follows); manual -> delete;
sap -> announce; reboot and OTA -> delete at shutdown, first announce
after boot only once the GM is known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The periodic TX timer had an arbitrary phase against packet boundaries,
so packets waited up to one packet time (different after every restart).
- One-shot esp_timer aimed at the next packet's due time from PTP, with
the clock re-read after sending.
- 1 kHz tone from a per-rate lookup table (one period = rate/1000
samples) instead of sinf() per sample.
Measured (RTP time - arrival, 4 ms vs 1 ms packets, expected -3.07 ms
incl. wire time): -3.3 ms extra before, now -3.14 ms. Tone within
1.0 LSB of the ideal PTP-phased sine, no gaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_tx: TX task woken by an esp_timer every packet time; sends every
packet whose last sample is in the past (PTP time), RTP ts =
(sample index from PTP + clk_offset) mod 2^32, packets aligned to
multiples of the packet size. Sends only while PTP is locked; resyncs
on lock, clock step or > 20 ms lag. L24/L16 big-endian, TTL/DSCP/
multicast IF from config; a config save restarts TX.
- Built-in 1 kHz tone at -18 dBFS, phase from the PTP sample index;
aes67_tx_set_source() pull callback for later sources (underruns
counted, padded with silence). status: tx_packets, underruns.
- Verified with a receiver script on the PC: 1 ms L24 stereo 1000/s,
0 gaps, ts step 48, tone -18.00 dBFS within 1.2 LSB of the ideal
PTP-phased sine; also L16 mono 0.333 ms, 0.125 ms (8000/s) and 4 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_sdp_sap: aes67_sdp_build() builds the AES67 SDP (RFC 4566 +
RFC 7273) with the same lines and order as the web UI preview;
GET /stream.sdp serves it as application/sdp.
- aes67_ptp: public aes67_ptp_gm_id(), aes67_ptp_locked(),
aes67_ptp_now_ns(). Clock IDs are now uppercase (RFC 7273 style), also
in status.ptp, which the UI copies into ts-refclk.
- aes67_net: aes67_net_netif() returns the AES67 interface.
- Verified on board: /stream.sdp byte-identical to the UI's sdp() for
the same config/status; mono, L16, ptime 0.333, ttl 8, clk_offset
4294967295 and session_ver all follow the config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Web UI PTP panel checked in the browser.
- GM loss test (ptp4l stopped): SLAVE -> LISTENING after the announce
timeout, frequency held; ptp4l restarted: UNCALIBRATED -> SLAVE in 22 s.
- Docs: P4 PTP implementation notes (UDP timestamp bit, RX hook, raw
Delay_Req, servo, measured results, link asymmetry).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- ptp_hw: ptp_hw_adj_freq() (ETH_MAC_ESP_CMD_ADJ_PTP_TIME, absolute ppb
vs nominal, clamped +-500 ppm, retried while the addend update is
busy) and ptp_hw_step() (read + write time).
- ptp_clock: first Sync after a GM is selected seeds the integral with
the measured rate and steps the clock; then linuxptp-style PI (kp/ki
from the Sync interval: 0.7/0.3 at 1 s). Re-step above 1 ms. Locked
after 8 Syncs with |offset| < 1 us, unlocked after 3 above; GM change
or loss unlocks and keeps the frequency (holdover).
- Verified vs ptp4l: stepped to GM time, locked after ~19 s; locked
offset within +-510 ns (mostly < 300 ns), correction +39.9 ppm
(crystal -39.8 ppm), path delay ~10.2 us.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ptp_clock.c replaces the 3.1 logger: UDP/IPv4 multicast, E2E.
Announce: IEEE 1588 dataset comparison picks the best GM, dropped after
announceReceiptTimeout x its announce interval. Sync/Follow_Up (two-step
and one-step) with HW t2 and correctionField. Delay_Req sent as a raw
frame (DSCP ptp.dscp, TTL 1, clockIdentity = EUI-64 from MAC) with HW
TX timestamp t3, randomised at the GM's Delay_Resp interval; Delay_Resp
matched on requestingPortIdentity + seq.
- Path delay corrected for offset drift between t2 and t3 (rate from
consecutive Syncs) so it is right before the clock is syntonised.
- ptp_hw: ptp_hw_send_event() builds Eth/IPv4/UDP 319 and returns the HW
TX timestamp.
- Verified vs ptp4l (i210 GM, HP 2530 non-PTP switch, PC 1G / board 100M):
GM selected, path delay settles at ~10.3 us and stays flat, rate
-39.8 +-0.4 ppm, offset drifts at -40 us/s (no servo yet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CONFIG_ETH_TRANSMIT_MUTEX was off, so LLDP (esp_timer task) could hit the
EMAC TX descriptors at the same time as lwIP. PTP adds a third sender.
Verified: OTA-installed build passes its self-test, web/status fine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aes67_ptp/ptp_hw.c: start the EMAC IEEE 1588 clock (IDF
ETH_MAC_ESP_CMD_PTP_ENABLE) and additionally enable snapshots for
PTP over UDP/IPv4 (IDF only enables PTP over Ethernet/L2). An RX hook
on the driver's info input path records {type, seq, sourcePortId, HW
timestamp} of port-319 PTPv2 event messages, then hands every frame to
lwIP unchanged.
- aes67_ptp.c: temporary 3.1 logger joins 224.0.1.129:319/320 and pairs
Sync HW RX timestamps with Follow_Up origin timestamps.
- Checked against linuxptp ptp4l (-4 -E -H, Intel igb) as GM: every Sync
has a HW timestamp; HW Sync intervals track the GM intervals with a
constant -39.8 us/s (+-0.3 us), i.e. local clock -39.8 ppm vs GM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test on board (ESP32-P4 rev v1.3):
- Rejected with 400 and nothing booted differently: garbage, wrong
project, min chip rev v3.0, truncated image.
- A (USB, ota_0) -> B via /api/ota: self-test passed, confirmed.
- Manual rollback B -> A; A -> B again.
- B -> C (forced self-test failure): C boots on trial in ota_0, fails,
rolls back to B in ota_1.
- D installed over the network; a later truncated upload hides
'previous' and clears can_rollback.
Docs: network flash command, serial-reset caveat, rollback test build,
OTA details in aes67-core-base.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/ota listed a half-written upload or a rolled-back image as
'previous'. Only report it when esp_ota_check_rollback_is_possible().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds an image whose self-test always fails, in its own build directory
(build-*/ ignored), for the step 2b rollback test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>