Repository navigation
Conversation
Code review on Task 2 caught this before Task 3 hit it: every sibling rule that compiles against a KitchenSync/main module includes this include path; test_follow_beat was the one exception, which would have surfaced as a misleading "follow_beat.h: file not found" once follow_beat.c/.h exist.
…k footprint From code review of the just-landed P4-020 follow_beat.c: 1. Mean-center the envelope window before correlating. Without this, any nonzero baseline loudness (continuous/sustained real audio, not just near-silent click trains) dominates every lag equally and confidence collapses toward ~1.0 regardless of actual rhythm strength -- the detector would never report valid on realistic continuous audio. Mean-centering the correlation also breaks the old confidence formula (best_corr / average-of-corr-across-lags): for sparse/click-like signals that average can go slightly negative once centered, flipping the ratio's sign. Replaced the normalizer with the window's own variance (the lag-0 autocorrelation), which is always >= 0 and numerically stable. This changes confidence's scale, so FOLLOW_BEAT_CONFIDENCE_THRESHOLD moves from 2.5 to 0.5 -- measured values cleanly separate noise (~0.13) from any periodic signal (~0.85-1.0) with this normalization. 2. Shrink analyze()'s stack footprint for the future capture task's tight 4096-byte FreeRTOS stack budget: corr[] now only spans the searched lag range (FOLLOW_BEAT_LAG_COUNT = 71 entries) instead of the full envelope length (400), and the win[] linear copy of the ring is eliminated entirely in favor of a ring_at() helper that indexes f->env_ring directly. analyze()'s own frame drops from ~3200 bytes to ~432-464 bytes (verified with -fstack-usage). Added test_detects_120_bpm_with_nonzero_baseline: a sustained nonzero baseline with a short louder pulse once per beat, at a known BPM -- this is the regression test for finding 1 (confirmed it fails against the pre-fix code, and only that test fails). All 7 follow_beat tests and all 36 other test suites pass.
Code review flagged (FollowBeatOut){0} in ks_web.cpp as a GNU extension
grafted onto C++ -- compiles under ESP-IDF's default gnu++ mode today, but
FollowBeatOut{} is equivalent and portable to any future toolchain/flag change.
…limitation Code review flagged two Important gaps: (1) TX/RX share one BCLK/WS pair but only one side actually drives it -- undocumented, now stated explicitly (TX/metronome is the driver, must stay enabled whenever RX/follow_beat runs). (2) init() failure paths leak handles and can't be retried -- acceptable today (no retry caller, runs once at boot) but flagged for Task 9's hardware step.
…start Code review caught a fail-safe regression: the pre-refactor codec_init() propagated a volume-set failure up (metronome stayed muted + logged). The shared-bus refactor dropped that check, so a failure would silently report the metronome ready anyway. Also refreshes the stale file-header comment that still described this file as owning I2S/codec bring-up directly.
Task 7 left this as a deferred NOTE pending Task 9. Since it's one-time, codec-level, hardware-fixed config (not something follow_beat_io.c should reach into the codec for per-enable), it belongs in audio_bus_init()'s codec_init(), not follow_beat_io.c. digital_mic=false (analog) matches the pre-P4-020 metronome_audio.c precedent; final confirmation against real hardware happens at the P4-020 hardware-validation step.
Code review findings: - Hedge the left-channel mic assumption the same way i2s_audio_bus.c already hedges digital_mic -- unconfirmed against real hardware, needs checking at the P4-020 hardware-validation step, degrades quietly (not a crash) if wrong. - Bounded backoff (vTaskDelay) on a read error instead of a bare `continue` -- today's ESP-IDF driver makes a fast-failing loop unlikely, but that safety is incidental to driver internals, not something this code enforces. - Disable the RX channel if xTaskCreate fails, instead of leaving it enabled with nothing draining it.
Code review caught a real end-to-end bug: /status always emitted follow_bpm/confidence/valid regardless of whether follow_beat_enable was on, and follow_valid is false both when the feature is off AND when it's on but not yet confident. The web UI's poll() only branched on follow_valid, so a user with the feature disabled would see the row flip from "off" to "listening..." within the first second and stay there -- implying the mic is active when it isn't. Fix: add a follow_enabled field to ks_status_json, independent of follow_valid, and have the JS branch on it first. Also grew the status buffer (192->220) to keep the truncation margin the extra field costs, and fixed a stale design-doc claim that follow_beat_enable was live-safe (it's reboot-only, matching the actual implementation).
…ts mic gain Hardware validation on a real Waveshare ESP32-P4-NANO found two real bugs the earlier (unvalidated) design missed: 1. i2s_channel_enable(TX) was gated on metronome_enable, called from metronome_audio_start(). With the metronome off, TX was never enabled -- and TX turns out to be the direction that actually drives the shared BCLK/WS clock. RX-only enable left follow_beat_io's capture task blocked forever in i2s_channel_read(), silently breaking the mic-only use case (the whole point of a "listen to ambient audio" feature) whenever the metronome wasn't also running. Fix: audio_bus_init() now unconditionally enables TX itself once the I2S/ codec bring-up succeeds, independent of whether the metronome FEATURE wants to click anything. An always-running, nothing-queued TX just emits silence (auto_clear, already configured). metronome_audio_start() no longer enables TX at all -- it only renders/queues click bursts onto a channel the bus already has running. 2. The ES8311's mic PGA gain defaulted too low/muted at power-on-reset -- confidence stayed exactly 0.0 (not just below threshold) even with a real signal present. Added an explicit es8311_microphone_gain_set(24dB) call in codec_init(). Post-fix, real hardware testing confirmed accurate detection (within ~1%) across 108/134/141/143/183 BPM with the metronome OFF, confirming the mic-only use case now genuinely works standalone. Also removes the TEMP DIAG left/right-channel logging used to diagnose this (confirmed: the ES8311 duplicates its mono ADC output into both I2S slots identically, so follow_beat_io.c's left-channel read was never the problem), and updates now-stale comments in i2s_audio_bus.h/.c that described the pre-fix "TX enabled by its own feature" design and a not-yet-confirmed mic wiring assumption.
P4-020: Follow Beat — mic-based tempo detection (display-only v1)
…rupt
ks_config_load() read the NVS blob straight into KsConfig. nvs_get_blob's size
handling is asymmetric: a stored blob LARGER than the buffer errors and copies
nothing, but a SMALLER one silently partial-fills and returns ESP_OK. So a
shrunk/reordered struct loaded bytes shifted into the wrong fields, and nothing
ever rejected it. (Confirmed against ESP-IDF v5.5 nvs_api.cpp via an Embedded
Firmware Engineer consult.)
Guard:
- KS_CONFIG_VERSION, stored under its own NVS "ver" key -- NOT a field inside
KsConfig as the ticket originally sketched. On the first upgrade from an
unversioned blob an in-struct version would read the old blob's first four
bytes (wifi_ssid[0..3], arbitrary ASCII) and could alias a plausible version.
A separate key makes "absent" an unambiguous "legacy, don't trust these bytes".
- ks_config_decode(): the one owner of "is this blob safe to load?". Accepts only
when the version key was present AND matches AND blob_len == sizeof(KsConfig)
AND ks_config_valid(). Anything else is clean defaults. Pure, host-tested;
ks_config_nvs.c hands over what NVS held and does no judging of its own.
- _Static_assert on sizeof(KsConfig): sizeof can't catch a same-size field
reorder (that's the version's job), but it makes editing the struct without
noticing the version constant a compile error.
- ks_config_save() commits blob + version together, so a power cut can't leave a
version stamp vouching for a blob that was never written.
- The rejection path now logs. A silent reset to defaults is indistinguishable
from "the user's settings vanished".
Hardware-validated on the deployed P4, which held a real pre-P4-014 blob:
W ks_config_nvs: stored config rejected (ver_present=0 ver=0 want=1,
size=228 want=228) -- loaded defaults, re-save to persist
Note size=228 want=228 -- the size matched. Only the missing version caught it,
which is exactly why sizeof alone was never sufficient. After one Save the blob
carries ver=1 and the next boot loads it silently.
Opt-in per unit (HAS_BATTERY_GAUGE): pure register decode + host-tested seam (battery_gauge.c, battery_snapshot.c, same shape as tempo_snapshot), thin I2C glue (battery_gauge_io.cpp), and a 3s poll task. web_status_json gains has_batt/batt_v/batt_pct, omitted entirely for boards with no gauge so the existing /status schema is untouched. CI compiles a QT Py + battery-gauge variant. Files LNK-040.
…ect budget Both X32Link and KitchenSync now set scan_method=WIFI_ALL_CHANNEL_SCAN before connecting — a passive/default scan can miss hidden SSIDs and some Wi-Fi 6 routers, leaving the STA stuck retrying a network it never sees. X32Link's mid-session reconnect moves off polling wifi_conn_policy_step() every loop() tick (which spammed esp_wifi_connect() and could make routers reject the STA) onto WiFi.onEvent() — disconnect/reconnect/got-IP all drive the policy from the actual radio event instead. Cold-start budget 30s -> 45s to give hidden SSIDs and slower APs room to associate.
Consolidates the web-based OTA flow (X32Link + KitchenSync) into one agent playbook: partition prerequisites, building the .bin, pushing over WiFi via curl or the browser, and auditing the deployed version before/after. Linked from both README.md and KitchenSync/README.md.
X32_emulator was buildable but never actually built in CI. Adds the native x32_port seam-test run (tests/) and an arduino-cli compile job for the XIAO ESP32-S3 profile, so a change that breaks the emulator is caught before merge instead of at the next manual on-device check.
Status bookkeeping catching up to already-landed work: ARC-004 declined (X32MidiClock retirement removed the dual-config sync hazard it was filed against), ESP-003/004/005 done (X32 emulator port), LNK-032 done (shared config-edit intake), P4-014 done (NVS versioning). P4-020 updated to note stage 1 (detect + display) shipped and stage 2 (publish into Link) is split off as new ticket ESP-007.
Toggling MIDI Clock Out off left its four Clock Out blocks on screen, while
Metronome and LED Strip collapsed correctly.
The desktop media query styles that group's section as a two-column grid:
@media (min-width:760px){ .grp--wide .sect{display:grid; ...} }
Two classes (0,2,0) out-specify .hide{display:none} (0,1,0), so the display
property never dropped to none. Only MIDI Clock Out carries .grp--wide, which
is why only it misbehaved -- and only above 760px, so a narrow window hid it
fine. syncSect() and the server-rendered `hide` class were both correct all
along; the class simply had no effect.
Make .hide authoritative. Verified on the device at both widths.
…, over WiFi
All three tiers of docs/plans/2026-07-08-p4-link-audio-feasibility.md passed in
one session on the ESP32-P4-NANO (WiFi via C6/ESP-Hosted -- the doc expected
Ethernet would be required; it wasn't, at 16kHz mono):
Tier 1 official SDK as tempo peer first build, zero SDK changes
Tier 2 LinkAudio channel in Live "P4 Mic" visible; Live's 5 channels
discovered on-device
Tier 3 mic audio audible in Live commit counter at block rate,
confirmed by ear
The port cost two real bugs, both found via panic decode on hardware:
- SDK's esp32 asio service task (confusingly also named "link") has a
hardcoded 8K stack sized for base Link; every LinkAudio processor runs on it
and overflows -- constant ~2KB stack-protection overshoot regardless of the
app task's size. Patched 8K->16K + renamed "link_io"; the SDK clone is
gitignored so the patch ships as patches/ableton-link-esp32-fixes.patch.
- The P4's 8KB TCM block carries MALLOC_CAP_INTERNAL, so best-fit can hand it
to a small FreeRTOS task stack (observed: ESP-Hosted's 5120B task); the
first flash op then asserts (stacks in TCM are invalid cache-off). Which
image loses this lottery is allocation-order luck -- KitchenSync wins it,
this one didn't. SOC_RESERVE_MEMORY_REGION removes the dice.
New pure module: beat_stamper (host-tested, 5 tests) -- continuous
beatsAtBufferBegin stamps: anchor to measured, advance by exact block length
in beats, resync only past 0.25-beat divergence. This was the feasibility
doc's "novel risk that never goes away".
Mic path reuses KitchenSync's P4-020-validated i2s_audio_bus.c by relative
path. GPLv2 SDK stays out of the tree and out of production firmware;
licensing email to Ableton remains the ship-gate.
Requirement (2026-07-10): >=44.1k, stereo. The PoC now streams 2ch/44.1k/int16 (1.41 Mbit/s) into Live -- verified audible, correct pitch, and a recording analyzed clean: zero dropped-sample gaps, no tonal noise, floor at -59 dBFS. Remaining artifact: 4 aperiodic clicks in 10s = WiFi packet jitter, which is the exact risk the feasibility doc assigns to Ethernet to fix (next step; the RJ45 is connected). - i2s_audio_bus: audio_bus_init(sample_rate) instead of the compile-time 16kHz -- KitchenSync passes AUDIO_BUS_SAMPLE_RATE, the PoC passes 44100. Single source preserved (ADR-0003), audio_bus_sample_rate() getter added. - MCLK multiple picked per rate family: the ES8311 coefficient table has the 48k family at rate*384 but the 44.1k family only at rate*256 -- rate*384 returned ESP_ERR_INVALID_ARG on hardware. - "Stereo" is format, not capture: the ES8311 ADC is mono, mirrored into both I2S slots (P4-020 finding), so both channels carry the same mic. True 2-channel capture is Tier 4 hardware (USB audio interface / external ADC). The downmix loop is gone -- the raw stereo DMA buffer commits directly.
…flutter) The "film projector" noise was diagnosed from a Live recording: a spectral comb at exact multiples of the block rate (86Hz = 44100/512) -- a sub-sample seam every block, produced by Live's receiver resampling our 44.1k stream into its 48k session while clock-skewed stamps fought it. A rate-matched recording measures clean: clicks 4 -> 0, comb gone, floor unchanged (-59.5 dBFS, room noise). Sessions aren't always 48k, so the PoC now adapts instead of hardcoding: - Channel announcements carry no rate; only received BUFFERS do. On first sight of a remote channel, subscribe a LinkAudioSource to it, read one buffer's Info.sampleRate, unsubscribe (destroyed off the Link thread), and reclock if it differs from ours. - audio_bus_reclock(rate) added to the shared bus: disable both channels, i2s_channel_reconfig_std_clock on each + es8311_sample_frequency_config, TX (the shared clock driver) re-enabled first, RX re-enabled by its consumer. MCLK multiple re-picked per rate family (44.1k family = x256). - beat_stamper re-anchors after a reclock; the straddling block is dropped. Sniff verified live against a 48k session (logs the sniffed rate, no reclock needed when equal). The reclock branch is untested against a real 44.1k session so far -- it logs loudly when it first fires.
…entity feat(x32link): publish dev=x32link — its own product identity
feat(p4): Link-mirror + fix /live silently dropping tempo (ESP-037 P4 parity)
…elds (ESP-042 pt1) First slice of retiring AppConfig: KsConfig gains the four fields that were AppConfig-only so ONE struct can serve both products. quantum_beats launch/phase quantize (the Touch's; the P4 hardcodes 4 today) transport_enable UI transport gate (the Touch's touch screen) play_on_release touch behaviour (Touch) lcd_brightness LCD backlight, distinct from led_brightness (the WS2812 strip) v4 -> v5 blob, MIGRATED not discarded: frozen KsConfigV4 + size assert, migrate_v4 (a memcpy of the v4 bytes plus the four defaults, since they were appended at the end). A P4 in the field keeps its config; the new fields default (quantum 4, transport on, fire-on-touch, LCD 80) -- what the Touch used, harmless on a P4 that ignores them. ks_config_set gains the quantum/transport/play_rel/bright form keys, and ks_config_live_safe_copy carries them (all live edits). Host-tested: v4->v5 migration preserves config + defaults the new fields; the form keys range-check. Next slices: the Touch stores KsConfig (with a final AppConfig->KsConfig migration), its web handlers fold onto ks_form_apply + ks_config_live_safe_copy, and AppConfig is deleted. Host suite green (52). P4 (esp32p4) builds. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
The correctness-critical core of retiring AppConfig: a pure, host-tested migration that
reads a legacy Touch NVS blob (versions 1..4) and produces the equivalent shared KsConfig,
so a Touch upgrading to the converged firmware KEEPS its WiFi + every setting instead of
dropping to the SoftAP portal. Same failure the freeze-and-migrate discipline exists to
stop -- ESP-013/030/037 each learned it the hard way.
ktouch_legacy_config.h the four OLD Touch layouts, FROZEN + size-asserted (152/316/
324/328), kept ONLY so this migration can read field bytes.
Read-only history; app_config.h's frozen structs move here so
app_config.h can be deleted next.
ktouch_config_migrate.c reads version N at N's frozen offsets, maps fields onto KsConfig
-- the single DIN output onto clock[0] (ppqn/nudge/swing), the
clock gate onto clock_out_enable -- and DEFAULTS whatever a
shorter (older) blob never carried (ppqn 24, swing 0, tempo
120000), so a migrated box's clock is byte-identical to before.
Fail-closed like the P4's decode: wrong size for the claimed version, a non-legacy version
(>=5 is native KsConfig), or bit-rot that fails ks_config_valid all return false with clean
defaults in *out -- never a half-migrated struct, never bytes read at the wrong offsets.
7 host tests: full v4 field map + WiFi survival, v3/v2/v1 defaulting the fields their layout
predates, v1's single credential to slot 0 with empty later slots, and the three rejects.
Next: the Touch's NVS glue stores KsConfig and calls this on an old `ver`; then the web
handlers/display/writer read KsConfig and AppConfig is deleted.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
…-042 pt3)
The Touch now stores the SHARED KsConfig; AppConfig and its parallel config system are
deleted. There is ONE config struct, ONE NVS migration lineage, and ONE live-safe field
list across the fleet -- the whole point of the ticket. A future config field is now a
single struct edit, not the same edit made twice with two chances to diverge.
ktouch_config_nvs.{h,cpp} NVS glue, "kstouch" namespace, storing a KsConfig blob.
ver >= KS_CONFIG_VERSION is a native KsConfig (ks_config_
decode); ver 1..4 is a legacy AppConfig blob, migrated ONCE
by ktouch_migrate_legacy_config and written back as v5. The
pre-ARC-020 field-key path maps straight onto KsConfig too.
ktouch_web.cpp handle_save + handle_live fold onto the P4's exact path:
ks_config_set (the shared grammar) builds a candidate, then
/live calls ks_config_live_safe_copy -- so the ESP-037 bug
class (a live field parsed, validated, then silently dropped)
cannot recur: a field is live IFF it's in that one copy.
/config.json emits g_config directly (no drift-prone
transient); /nudge + /bright go through ks_config_set.
.ino / display / midi_out / tempo g_config is a KsConfig; field reads remap onto it
(brightness->lcd_brightness, nudge->clock[0].phase_mbeats,
ppqn/swing->clock[0], clock_enable->clock_out_enable).
The Touch drives a SINGLE DIN output, so its per-output settings live on clock[0]; the
master "MIDI Clock" toggle is clock_out_enable and clock[0].enable is pinned on. clk0_en
and clock_out from the app both mean "the clock" on a one-output box.
test_ktouch_config deleted -- it covered the retired AppConfig; KsConfig is covered by
test_ks_config and the migration by test_ktouch_migrate.
Verified: host suite green; P4 (esp32p4), BOTH Touch variants (Super Mini headless +
Waveshare LCD), and X32Link all compile. Remaining acceptance item is the hardware smoke
on the bench Super Mini -- flash it and confirm its saved WiFi + settings survive the
AppConfig->KsConfig migration (needs the board on the bench).
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
Flashed the converged firmware over the board's real AppConfig-v4 blob; NVS dump confirms ver 4->5, blob 328->456 B, WiFi preserved, tempo 143.000 BPM (non-default) carried across. All acceptance criteria met. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
ESP-042: converge the Touch onto the shared KsConfig — retire AppConfig
…0-0014 Documentation-only. No runtime, build, API, or protocol changes. - docs/architecture/system-overview.md — platform architecture source of truth - docs/architecture/repository-roadmap.md — ks-* direction with extraction triggers - docs/architecture/alignment-report.md — aligned / tech-debt / future-evolution - docs/architecture/validation-report.md — proposal validated against the code - docs/contracts/firmware-http-contract.md — firmware<->client HTTP + Bonjour API - docs/adr/0010-0014 + docs/adr/README.md — continue the existing 0003-0009 numbering - README.md / AGENTS.md — additive pointers to the new docs (no duplication)
docs: architecture initiative — platform overview, contract, ADRs 0010–0014
Tracking-only. Cross-references the convergence ticket to the now-merged decision record and alignment/roadmap docs. No code or behavior change. Co-authored-by: Eric Dahl <edahl@nd.edu>
… ESP-042 storage) Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
…s (ESP-040)
X32Link's handle_save read input_source / midi_clock_out / phase_flash straight off
server.arg() with no guard. An absent key decodes to 0 -- a LEGAL value for all three --
so a partial POST (the iOS app saving a WiFi network, a subset form) silently reset the
tempo source to Link, disabled the MIDI clock, and flipped the phase display to sweep.
Same bug class the P4 (ks_form_resolve) and the Touch (full_form sentinel) already fixed;
X32Link had adopted neither.
Decision: keep X32Link's own AppConfig -- it's a mixer bridge, a separate product, and
mixer_ip/model/fx_slot/fdr have no honest home in the clock-box KsConfig. Instead give
AppConfig the same treatment KsConfig has:
app_config_set_kv string-keyed setter in the SHARED web vocabulary -- clock_out,
led_beat, led_accent (what /config.json emits) plus the form's own
aliases. This is the round-trip fix: a client can now read the config
and POST it back under the same names. Ints route through
app_config_set (config_validate stays the one range owner); a blank
ssid/pass/ip is "keep current".
x32_form_merge pure partial-vs-full-form merge mirroring ks_form. full_form=false is
a PATCH (absent keys unchanged -- the bug fix); full_form=true (the
device's own page) pre-clears the two checkbox-backed booleans so an
unchecked box means off. Applies `model` before `fx_slot` so the slot
validates against the new model's max regardless of POST order.
handle_save now routes through these; the form gained a `full_form` hidden sentinel so
"absent = off" applies ONLY to the device's own full submission, never a partial patch.
handle_live was already hasArg-guarded, so it was left as-is.
9 new host tests (partial-preserve, full-form on/off, value-fields-not-zeroed, round-trip,
keep/reject edges, model-before-fx_slot). Host suite 575/0; both X32Link variants compile.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
…esolver ESP-040: X32Link /save — stop silently zeroing fields on a partial POST
… (ESP-043)
A screenless clock box gave the operator no local feedback -- stopped, armed, running, or
Link-locked, you had to open the app. The S3 Super Mini has one unused onboard WS2812 (GPIO48,
the esp32s3 variant's PIN_RGB_LED); drive it as a status light so the box says what it's doing
across a room. (Surfaced when the operator saw the onboard LED "not blinking in time" -- nothing
drove it: KSTOUCH_HAS_LED is the external strip, transport_led is the DevKit's buttons.)
ktouch_status_rgb.{c,h} the STATE -> COLOUR decision as a PURE, host-tested function -- not
buried in loop(), same rule that keeps tempo/transport mapping out of
views. dark stopped; amber blink armed (cocked hammer); green beat-
flash running; cyan when Link-locked (blue channel is the "locked"
tell, so free-run keeps blue at 0); brighter same-hue accent on the
bar-1 downbeat. Hard-edged flash (first ~14% of the beat), a hammer
not a throb.
config.h HAS_STATUS_RGB + STATUS_RGB_GPIO 48, gated to BOARD_S3_SUPERMINI only
(the Waveshare's GPIO48 is its panel; the DevKit has none).
.ino thin status_rgb_tick() samples transport/lock/beats/quantum, calls the
pure fn, writes the LED only on a colour change.
5 host tests (stopped-dark, flash on/off, green-vs-cyan, downbeat brighter, armed amber). Host
suite 580/0; both Touch variants compile. Verified on the bench Super Mini over OTA: amber armed,
cyan beat-flash running+Link-locked, green free-run, dark stopped.
Follow-on: the operator has RGB-in-button switches coming for the Super Mini's transport surface;
those per-button LEDs should reuse this colour vocabulary so onboard + buttons say the same thing.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
… explicitly macOS links libm implicitly so it passed locally; the Linux CI runner does not, so the host-test build failed at link. Every other math-using test rule already has -lm. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
ESP-043: onboard RGB as a status light on the headless Super Mini
X32Link/ is both a shipping mixer product AND the home of the shared pure-C engine; the P4 flagship's CMake reaches into ../../X32Link/ for its core, the Touch symlinks 53 files, and the code-memory index labels all 12 clusters "X32Link" because the folder is the package boundary — it can't tell the platform core from a mixer feature. The conflation caused real defects (ESP-040, ESP-042), and X32Link is a separate product for a separate audience. ADR-0015 proposes moving the shared engine to a neutral in-repo shared/ (the reversible precursor to the ks-core repo, ADR-0014 step 2 — not that extraction). Honest about the trade-off: the P4 decouples for free (one CMake line), but Arduino consumers (X32Link, the Touch until ESP-026) need symlinks to shared/. Two sequencings — Option A bundles the move with ESP-026 so the Touch's symlinks vanish (symlink-neutral, recommended); Option B does it standalone now (faster ownership fix, throwaway churn). Includes the migration plan (manifest, git-mv, re-point map, verification, re-index) and reopens ARC-009 (declined 2026-07-05; its trigger has now fired). Proposed, not accepted — the sequencing call is deliberately left for review. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
ADR-0015: extract ks-core in-repo out of X32Link/ (accepted, Option A)
…y/stop, hold=realign (#137) Files the v1 firmware half of the Mini's live surface: a single button on the headless Super Mini (short press toggles play/stop, ~600ms hold posts realign), reusing the existing HAS_BUTTONS mailbox path and ESP-043 onboard RGB for status. Input-only — no new transport logic. Pairs with ESP-021 (enclosure + panel-mount button/TRS). Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6 Co-authored-by: Eric Dahl <edahl@nd.edu>
…(ESP-044) (#136) The KSTouch-Config setup AP was invisible to a phone a foot away on the headless Super Mini even though WiFi.softAP() returned true (start=1, ch=1, board stable). Two causes underneath the truthy return: AP-mode modem sleep gating the beacon, and the Super Mini's weak PCB antenna at default TX power. Set WiFi.setSleep(false) + WiFi.setTxPower(WIFI_POWER_19_5dBm) in start_ap(); log a hard softAP() failure. Shared with the Touch product board — harmless (stronger beacon) there too. Verified on the bench Super Mini: the SSID now appears and onboarding proceeds. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6 Co-authored-by: Eric Dahl <edahl@nd.edu>
* spike(ESP-026): display increment 1 — panel bring-up on ESP-IDF (S3)
Port the KitchenSync Touch 1.47" LCD to the ESP-IDF firmware. Increment 1 proves the
LovyanGFX Panel_ST7789 + Bus_SPI config comes up under ESP-IDF's SPI on the S3.
- ks_display.{h,cpp}: LGFX config copied verbatim from ktouch_display.cpp; backlight on
ledc (was analogWrite); render task draws a test frame. No touch / live data yet.
- idf_component.yml: LovyanGFX managed component, esp32s3 target only.
- Kconfig: KS_TOUCH_DISPLAY (depends on IDF_TARGET_ESP32S3, default n).
- CMakeLists: always compile ks_display.cpp (no-op stub when flag off).
- ks_main.c: one guarded call + include; P4 build unaffected.
Spike — not for merge until verified on glass.
* spike(ESP-026): S3-only sdkconfig defaults — enable KS_TOUCH_DISPLAY
sdkconfig.defaults.esp32s3 auto-loads only for the S3 target, so a fresh
'idf.py set-target esp32s3' comes up display-enabled and P4 is untouched.
* spike(ESP-026): pull LovyanGFX as pinned git dep (1.2.21), not a registry component
LovyanGFX is not in the Espressif component registry (the ^1.1.16 registry pin
failed to resolve). Pull it from GitHub pinned to tag 1.2.21 — the exact version
the Arduino firmware already ships (THIRD_PARTY.md, BSD-2-Clause). Gated to the S3
target.
* spike(ESP-026): fix -Werror=misleading-indentation in ks_display
Split the two-statement brightness clamp onto separate lines. ESP-IDF builds with
-Werror=misleading-indentation (Arduino did not). Full esp32s3 build now green.
* tasks(ESP-026): record spike result — display up on ESP-IDF (verified on glass)
* spike(ESP-026): display increment 3 — AXS5106L touch + USB-Serial-JTAG console
- ks_display.cpp: AXS5106L over i2c_master (SDA42/SCL41, 0x63) + GPIO47 reset,
reusing the host-tested axs5106l.c parser (added to CMake SRCS by path). Taps are
echoed on screen ('TAP x,y') and to the log.
- sdkconfig.defaults.esp32s3: route the console to USB-Serial-JTAG (the Arduino
build's hwcdc). ESP-IDF's UART default left the USB port dark after boot, so the
cu.usbmodem port vanished and esptool could not auto-reset. Now it persists +
serial logs work. (Builds green; hardware verification pending the next flash.)
* tasks(ESP-026): touch + USB-Serial-JTAG console verified on hardware
* spike(ESP-026): map touch to landscape screen coords + dot under finger
Portrait-native AXS5106L coords -> setRotation(7) landscape: sx=319-raw_y,
sy=171-raw_x (verified against a corner tap). Draws a dot at the mapped point.
* docs(ESP-026): re-verify display spike on master + record the download-mode gotcha
Rebased the display spike onto master (clean), rebuilt esp32s3, reflashed the Waveshare
S3-Touch — panel shows "ESP-IDF DISPLAY OK". Landing the spike. Note the board latches
DOWNLOAD mode on every host-issued USB reset (esptool/monitor/DTR), so a fresh flash boots
dark and silent until a physical power-cycle — looks like a crash, isn't one.
Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6
---------
Co-authored-by: Eric Dahl <edahl@nd.edu>
…tart (ESP-026) (#139) Realign was an explicit no-op in ks_tick.c: transport_launch produced TL_RESTART but plan.transport[] held one action per output, so the writer dropped it and emitted nothing on the bar line. Add TRANSPORT_RESTART; ks_tick_out_bytes expands it to 0xFC then 0xFA, both before the downbeat 0xF8 (the ESP-038 transport-before-clock invariant). Closes the ESP-026 TL_RESTART acceptance criterion. TDD (test/test_ks_tick.c): out_bytes emits [0xFC,0xFA]; the pair precedes the first clock; and a realign posted while running + on the bar line drives TRANSPORT_RESTART through the full tick. All 60 host suites green. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6 Co-authored-by: Eric Dahl <edahl@nd.edu>
…P-045) (#140) The Mini's one live button needs two gestures: short press = play/stop, hold = realign. transport_button_update() splits a debounced press into TAP (on release) or HOLD (fired once at a 600ms threshold, while still down; the release then stays silent). Pure timing over buttons.c debounce — the glue owns only the pin read and mapping TAP->toggle / HOLD->realign, so the short-vs-long decision is host-tested. Lives in KitchenSync/main/ (transport is a KitchenSync concept, not X32Link). TDD: test_transport_button.c — tap, hold-once-then-silent-release, and back-to-back gesture reset. All host suites green. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6 Co-authored-by: Eric Dahl <edahl@nd.edu>
…inc 1) Pure per-output LED dispatcher: renders the METRONOME style via metro_strip_render at full scale, then applies bright once over the buffer -> the dispatcher is the single owner of brightness (kills the future brightness^2 double-apply). Explicit style switch; STATUS/EFFECT blank stubs until ESP-048/050 (never fall through to metronome). Host-tested (test_led_output, 6 cases). metro_strip + all glue untouched. Increment 2 (core-0 LED task + reroute P4 caller + drop metro_strip's own bright) needs the P4 bench, deferred.
clock_out_task now only publishes an s_led snapshot (active/standby/beats + cfg, plain us stores). New core-0 low-pri led_task reads it and does the render + blocking RMT push -> a slow/long strip push can no longer jitter the 1 ms clock (core 1, pri 19). Active path renders via led_output (brightness owner, metro_strip @100); standby keeps metro_strip_standby. Behavior-preserving. Verified on P4 hardware: boots clean, clock stable (61 pulses/s @152 BPM free-run), zero drops/overruns. metro_strip + all pure logic untouched.
Skeyelab
force-pushed
the
feat/esp-046-led-dispatcher
branch
from
August 23, 2026 13:17
351f339 to
7e3840a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ESP-046 — per-output LED dispatcher foundation (increment 1)
WIP / draft. Increment 1 only: the pure
led_outputdispatcher, host-tested. Increment 2 (core-0 LED task + reroute the P4clock_out_taskthrough the dispatcher + dropmetro_strip's ownbright) needs the P4 on a bench and is deferred.What's here
X32Link/led_output.{h,c}— pure dispatcher: renders the METRONOME style viametro_strip_renderat full scale, then appliesbrightonce over the buffer → the dispatcher is the single owner of brightness (kills the future brightness² double-apply). Explicit styleswitch; STATUS/EFFECT are blank stubs (ESP-048/050) and never fall through to the metronome pattern.test/test_led_output.c— 6 behavioral host tests (full-bright passthrough, bright=50 halves, bright=0 dark, disabled dark, FLASH pattern+fade, unimplemented-style dark).Not touched
metro_strip.{h,c},ks_led.*,ks_main.c,KitchenSyncTouch/*, all glue/hardware. No behavior change on any board.Verify
cd test && make— whole host suite green,test_led_output6/6.Design + tickets: #141. Ticket: ESP-046.