Commit Graph

30 Commits

Author SHA1 Message Date
bsncubed 856d062269 status: hls download stats (last segment speed, failed requests, last error)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:58 +10:00
bsncubed 66e72cebcc status: spotify_error, the last Spotify session error and when it happened
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:59:40 +10:00
bsncubed d4aa9c7be1 PTP status: 2-min average offset, details split; uptime as d/h/m/s
- Firmware: status.ptp.offset_avg_ns = mean |offset| over the last 2 min
  (one bucket per second, cleared on a clock step).
- Main panel: Time Offset (2 min avg) shows only the Bolero bracket of the
  average. Details: the current offset (coloured by the same brackets),
  hops, time/frequency traceable, version and own clock class.
- Uptime shown as "1d 2h 3m 4s".
- Preset buttons renamed: "Riedel PTP defaults", "AES67 Media PTP defaults".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 08:32:15 +10:00
bsncubed 5f27262b27 PTP: drop the "Preferred TimeTransmitter" role
It behaved exactly like auto (clock class 248); the only difference was a
priority1 suggestion in the page. Roles are now slave|auto; to prefer this
device, use auto with a low priority1. A stored "master" is converted to
auto at boot (new cfg_set_string, like cfg_set_number).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:58:26 +10:00
bsncubed ee03d9f9f1 PTP: hardware timestamping is not a setting any more
The hw_ts config key was never read (hardware timestamps are always used);
the checkbox suggested it could be turned off. Removed from the config,
the page and the docs; status.ptp.hw_ts still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:55:54 +10:00
bsncubed 214d6b2c29 Firmware version: <version.txt>-<git short hash>, starting at 0.0.1
e.g. 0.0.1-1d613b1: the first part is set by hand, the hash says which
commit was built. No dirty flag (the build always patches cspot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:52:23 +10:00
bsncubed 1d613b16c1 Firmware version from version.txt (0.9.0) instead of git describe
The version is now set by hand in version.txt, so it's readable and only
changes on purpose. git describe gave commit hashes, and always "-dirty"
because the build applies patches to the cspot submodule. Editing the file
is enough; the next build picks it up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:41:53 +10:00
bsncubed 05f4ce45c3 48 kHz only: drop 96 kHz from the AES67 output
All sources (player, test signals) are 48 kHz; at 96 kHz Spotify and HLS
played at the wrong speed. Validation, the page and the docs allow 48000
only, and a 96000 stored before is reset to 48000 at boot (stored values
aren't re-validated, and the SDP would still have said 96 kHz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:30:39 +10:00
bsncubed 62b32c31ff Docs: end-user guide; README brought up to date
docs/user-guide.md covers setup, the web page, sources and failover, the
Spotify developer app, AES67 output and SDP/SAP, PTP presets, network and
static IP, logging, firmware updates (OTA and USB), the player API,
troubleshooting and known limitations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:26:52 +10:00
bsncubed d744a36275 Web UI: dark by default with a light/dark toggle; fix hidden labels
The page starts dark regardless of the system setting; the toggle at the top
right switches to light and is remembered in the browser. [hidden] now
always wins: label{display:block} had overridden it, so the hidden VLAN
split checkbox still showed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:16:04 +10:00
bsncubed c4468c4af0 Network: status reports mask/gw/dns; UI shows the DHCP lease in the static fields
With DHCP on, the greyed-out IP/netmask/gateway/DNS fields show the
addresses in use. Unticking DHCP keeps them as the starting point for a
static setup. (Static addressing itself is not applied yet.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:42:35 +10:00
bsncubed 122e46e862 Pink noise as a source (mode "pink")
Core: aes67_tx_pink_noise(), a ready-made TX source: xorshift32 white noise
through Paul Kellet's pink filter, -18 dBFS RMS (peaks about -6 dBFS), same
on L and R. The player uses it like the tone (straight to TX, no volume or
gain). Checked on the board: -18 dBFS RMS, octave bands 63 Hz-4 kHz flat
within +-0.4 dB, L = R.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:37:05 +10:00
bsncubed c9e06da47c aes67_tx: mono sum; the source always delivers stereo
The pull callback's contract said "stream channels", but the player always
delivers stereo, so a 1-channel stream went out as L,R,L,R at half speed.
Sources now always deliver AES67_TX_SRC_CHANNELS (2); TX maps to the stream:
2 ch as is, 1 ch = (L+R)/2 in 64 bit with mono_sum (can't clip), else L.
Checked: mono packets 156 B, 1000/s, 48 samples each, SDP L24/48000/1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:26:48 +10:00
bsncubed 8485ecb5b7 Player: transport commands on /api/player
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>
2026-09-25 22:59:14 +10:00
bsncubed cd747aec40 Step 7.5b4a: player volume (Spotify app slider), applied after the ring
- 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>
2026-09-25 18:52:57 +10:00
bsncubed 7bbda0a1e8 Step 7.5a: Spotify client credentials in config/UI, write-only secrets
- 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>
2026-09-25 15:16:42 +10:00
bsncubed d3897f3179 Step 7.1: player plumbing (PSRAM ring -> AES67 TX), test tone mode
- 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>
2026-09-25 14:36:15 +10:00
bsncubed e763dc81bd Phase 2: NTP with hostnames (pool.ntp.org) or IPs
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>
2026-09-25 08:22:58 +10:00
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
bsncubed dcea39b216 Step 6.2: role hand-over, session_ver bump on GM change
- 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>
2026-09-25 08:08:00 +10:00
bsncubed e2dc7552c2 Build order: step 5 done without VLAN; VLAN split parked for phase 2
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>
2026-09-25 07:26:03 +10:00
bsncubed f6dfa80b19 Step 5 (health): temperatures in status.temps
- 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>
2026-09-25 07:20:02 +10:00
bsncubed f3779696d5 Step 5 (SAP): announce the stream via SAP
- 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>
2026-09-25 06:57:47 +10:00
bsncubed 004cc6e212 Step 3 verified: PTP TimeReceiver locks, status in the UI, GM loss handled
- 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>
2026-09-25 06:26:44 +10:00
bsncubed f259c5d554 Step 2b verified: OTA update, rejections and rollback on board
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>
2026-09-24 23:28:55 +10:00
bsncubed 07e16e6163 DHCP hostname: send net.hostname in DHCP requests
- aes67_net: esp_netif_set_hostname() before the interface starts, and
  in the net apply callback (takes effect at the next lease renewal).
- Verified: the router's DNS (lan.apointless.space) resolves p4-aes67
  to 192.168.192.244.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:16:39 +10:00
bsncubed 30c32e702b Step 2a (2/2): LLDP transmit, switch shows name and IP
- aes67_net/lldp.c: IEEE 802.1AB LLDPDU sent as a raw frame with
  esp_eth_transmit every 30 s (TTL 120 s) and immediately on a new IP
  or hostname change. TLVs: chassis/port ID (MAC), port description,
  system name (net.hostname), system description (project + version),
  capabilities station-only, IPv4 management address.
- Docs: LLDP in the core Network section; step 2a ticked.
- Verified on an HP 2530-8G-PoE+ (show lldp info remote-device 2): all
  fields shown, management address 192.168.192.244.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:14:09 +10:00
bsncubed 979420a6bb Step 2a (1/2): mDNS hostname.local and _http._tcp
- aes67_net: espressif/mdns 1.13.1 (managed component, pinned in
  dependencies.lock). Advertises <net.hostname>.local and the web UI as
  _http._tcp port 80. net apply callback renames live.
- First config value actually applied: net.hostname.
- Docs: mDNS in the core Network section; build order gets step 2a
  (mDNS, then LLDP).
- Verified on board: p4-aes67.local resolves via raw mDNS and getent,
  http://p4-aes67.local/ answers, service shows in _http._tcp browse;
  live rename to p4-rename and back works (after ~1 s probing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:51 +10:00
bsncubed bbfd3506bf Step 0: IDF project skeleton for esp32p4 with core component stubs
- Board checked with esptool: ESP32-P4 rev v1.3 (engineering silicon),
  32 MB GigaDevice flash. Rev <3 support and 32 MB set in sdkconfig.defaults.
- OTA partition layout: nvs, otadata, phy_init, ota_0/ota_1 (6 MB each,
  below 16 MB), coredump; no factory app. Partition table at 0x10000 so
  the bootloader can grow.
- App rollback enabled in the bootloader from the start.
- PROJECT_VER from git describe --tags --always --dirty.
- Empty stubs for the nine aes67_* core components; main logs version
  and chip revision.
- Built with ESP-IDF v5.5.5; not yet flashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:47:21 +10:00
bsncubed 67f76aed93 Upload files to "docs" 2026-09-23 21:52:22 +10:00