Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .american-spelling-allow
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# American spelling checker allowlist (see ericdahl-dev/.github scripts/american_spelling.py)

# Vendored ThrowTheSwitch Unity test framework: third-party code, kept as upstream ships it.
path: lib/unity/
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ Keys: `↑`/`↓` tempo ±1, `←`/`→` tempo ±0.1, `space` start/stop, `q`/`Q
`p` force beat 0 onto *now* (a known downbeat to trigger the analyzer on), `s` start/stop
sync, `a` Link on/off, `x` quit. Flags: `--tempo N`, `--quantum N`, `--play`.

Two behaviours that will otherwise read as firmware bugs:
Two behaviors that will otherwise read as firmware bugs:

- **A peer that joins an already-playing session stays stopped.** This is Link's design,
not a bug: start/stop is last-writer-wins by timestamp (`Controller.hpp:89`), and a
booting peer initialises its start/stop state with `timestamp = hostTime`
booting peer initializes its start/stop state with `timestamp = hostTime`
(`Controller.hpp:67`) — so its fresh "stopped" is *newer* than an earlier "playing" and
wins. Tempo, by contrast, *is* adopted by late joiners, which makes the asymmetry
extra confusing. **Form the session first, then press play.** A device that reboots
Expand Down
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ distributed .bin traces back to source.
JSON `"fw"` field (fleet-auditable by script), and the `/update` OTA page
shows the running version before you flash over it. One shared
`fw_version.h` across X32Link, KitchenSync, and the X32 emulator.
- Beat LED colours (LNK-039): on RGB-LED boards (QT Py NeoPixel) the beat LED
follows the web UI's BEAT/BAR1 colour pickers — bar-1 downbeat gets the
accent colour, live-applied with no reboot. The WiFi-down diagnostic blink
stays fixed green so a dark user colour can never hide it.
- Beat LED colors (LNK-039): on RGB-LED boards (QT Py NeoPixel) the beat LED
follows the web UI's BEAT/BAR1 color pickers — bar-1 downbeat gets the
accent color, live-applied with no reboot. The WiFi-down diagnostic blink
stays fixed green so a dark user color can never hide it.
- CI now compile-checks the QT Py NeoPixel board profile (`LED_RGB` path)
alongside the headless and Waveshare touch variants.

Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ clock source to verify: **MIDI clock IN → BPM** (P4-011 stage 1).
**Link Audio** (P4-032) — the P4 can stream its mic into an Ableton Live 12.4
session as a Link Audio channel, in sync. That lives in `../LinkAudioPoC/`, not
in this firmware, because it links Ableton's **GPLv2** SDK. Integrating it means
clearing a commercial-licence conversation with Ableton first. Three tickets
clearing a commercial-license conversation with Ableton first. Three tickets
(P4-032 productization, ESP-008 USB interface ingest, ESP-010 transport
proposal) are blocked on that one email.

Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/main/ks_display.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
extern "C" {
#endif

// Initialise the panel + backlight and spawn the render task. Idempotent-safe to call
// Initialize the panel + backlight and spawn the render task. Idempotent-safe to call
// once from app_main. No-op unless CONFIG_KS_TOUCH_DISPLAY.
void ks_display_start(void);

Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/main/ks_led.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
extern "C" {
#endif

// Initialise the RMT WS2812 device on `gpio` for `npix` pixels. Safe to call once
// Initialize the RMT WS2812 device on `gpio` for `npix` pixels. Safe to call once
// at boot even if the LED feature is off; a failure logs and disables show/clear.
void ks_led_start(int gpio, int npix);

Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/main/ks_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ static void status_task(void *arg)
(unsigned long)s_stat.usb_dropped);
/* P4-038: the counters are LIFETIME now (/status owns them), so this no longer
* zeroes them. Log on the EDGE -- only when the count has grown since the last
* pass -- which keeps the old "shout when something happened" serial behaviour
* pass -- which keeps the old "shout when something happened" serial behavior
* without a log per overrun. (A log per overrun is a blocking ~13 ms UART write
* that delays the next tick, which overruns, which logs: the feedback loop that
* manufactured 3945 phantom overruns in 40 s on the Touch.) */
Expand Down
4 changes: 2 additions & 2 deletions KitchenSync/main/ks_tick.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

void ks_tick_reset(KsTickState* st, uint32_t cfg_gen) {
beat_source_reset(&st->src);
/* P4-038: INITIALISE the lifetime counter. Callers do `KsTickState ts;
/* P4-038: INITIALIZE the lifetime counter. Callers do `KsTickState ts;
* ks_tick_reset(&ts, gen);` — leaving this alone means it starts as stack garbage and
* can never be trusted, the same trap clock_ticker.c calls out for `dropped`. The host
* test caught precisely that: a phantom +1 on a fresh state. */
Expand Down Expand Up @@ -93,7 +93,7 @@ KsTickPlan ks_tick_step(KsTickState* st, const KsTickInputs* in) {
// Arbitration, PER OUTPUT: exactly one master owns each output's transport.
// Link owns an output once the session publishes StartStopState AND that
// output is set to follow it (cfg.clock[o].follow_link, the default); such
// an output ignores manual presses and the UI greys its toggle. An output
// an output ignores manual presses and the UI grays its toggle. An output
// with follow_link=0 is manual: its web toggle drives it and the session's
// transport never touches it, so a drum machine can follow Ableton while a
// synth is launched by hand two bars later (ESP-011's whole point).
Expand Down
14 changes: 7 additions & 7 deletions KitchenSync/main/ks_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ color:var(--ink);cursor:pointer}
.tp.armed{border-color:var(--amber);color:var(--amber)}
.tp.running{border-color:#7fbf1f;color:var(--led)}
.tp:disabled{opacity:.4;cursor:not-allowed;border-color:var(--line);color:var(--mut)}
/* ESP-011: per-output transport toggle — one control, state is the label/colour
/* ESP-011: per-output transport toggle — one control, state is the label/color
(the KitchenSync Touch device's full-screen toggle, shrunk to a row). */
.tgl{flex:1;font-family:var(--disp);font-weight:800;font-size:13px;letter-spacing:.14em;
padding:11px 12px;border-radius:9px;cursor:pointer;border:1px solid var(--line);
Expand Down Expand Up @@ -146,7 +146,7 @@ border:1px solid var(--line);border-radius:5px;padding:2px 6px;margin-left:8px;b
.sect[data-when="clock_out"] .grid2{grid-template-columns:minmax(0,1fr)}
/* Wide viewports (desktop): widen the card and pack the config form into two
masonry-flow columns so it isn't a long, skinny strip. Column-flow (not grid)
keeps a short group from waiting on a tall neighbour's row height. Status
keeps a short group from waiting on a tall neighbor's row height. Status
readout stays full-width above. */
@media (min-width:760px){
.unit{max-width:900px}
Expand Down Expand Up @@ -258,7 +258,7 @@ fetch('/transport?out='+b.dataset.out+'&play='+b.dataset.play,{method:'POST'}).c
// Per-output transport (ESP-011). Each output has ONE master: Link when its
// "follow Link" switch is on and the session publishes transport, otherwise you.
// A Link-owned output shows the SESSION's play state (its manual launch state is
// frozen and would lie) and its toggle greys out.
// frozen and would lie) and its toggle grays out.
function outFollows(o){var cb=document.querySelector('input[name="clk'+o+'_follow"]');return !!(cb&&cb.checked)}
function showLaunch(a,d){
Array.prototype.forEach.call(document.querySelectorAll('.tgl'),function(b){
Expand Down Expand Up @@ -396,7 +396,7 @@ static std::string build_outputs()
+ "><span class=\"track\"><span class=\"knob\"></span></span><span class=\"swlbl\"></span></label>";
// ESP-011: one Touch-style toggle per output (stopped/arming/playing), plus
// the per-output transport master. Follow Link on => this output tracks the
// session and its toggle greys out; off => the toggle is yours.
// session and its toggle grays out; off => the toggle is yours.
// ESP-019: the key that fires this toggle is printed on it. Clock Out 1..4 are
// data-out 0..3, so the badge is the 1-based label the user already reads.
s += std::string("<div class=\"fld\"><span class=\"pre\">RUN</span>")
Expand Down Expand Up @@ -456,7 +456,7 @@ static std::string build_voice()
return s;
}

// LED-strip customization controls (P4-019): brightness, pattern, fade, colours —
// LED-strip customization controls (P4-019): brightness, pattern, fade, colors —
// all on the /live path so they apply instantly with no reboot.
static std::string build_led()
{
Expand Down Expand Up @@ -748,7 +748,7 @@ void ks_web_config_persist_tick(void)
if (!s_cfg) return;

/* ESP-037: while a Link session is DRIVING, remember its tempo as ours -- the same
* behaviour the Touch has. Link teaches the box the tempo; when Link goes away the
* behavior the Touch has. Link teaches the box the tempo; when Link goes away the
* box keeps playing it (the arbiter's free-run seed, applied by the clock task) AND
* persists it, so a power-cycle comes back at the last tempo it actually played.
*
Expand Down Expand Up @@ -869,7 +869,7 @@ void ks_web_start(KsConfig* cfg, volatile uint32_t* gen, SemaphoreHandle_t cfg_m
httpd_handle_t server = NULL;
httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG();
// P4-037: purge the least-recently-used connection when httpd's pool is full, rather
// than refusing the new one. Defence in depth beside the real fix (LWIP_MAX_SOCKETS=16
// than refusing the new one. Defense in depth beside the real fix (LWIP_MAX_SOCKETS=16
// in sdkconfig.defaults): the pool is 7 with 3 reserved internally, so 4 client slots,
// and a desktop browser opens up to 6 keep-alive connections to one host.
hcfg.lru_purge_enable = true;
Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/main/metronome_audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ void metronome_audio_start(int volume, int voice)
bool metronome_audio_ready(void) { return s_ready; }

/* P4-029: re-apply volume + voice at runtime (no reboot) so the web /live path can
* preview them like the LED colours. No-op until _ready() — the codec/I2S are only
* preview them like the LED colors. No-op until _ready() — the codec/I2S are only
* brought up at boot when the metronome was enabled, so enabling it still needs a
* reboot; only volume/voice of an already-running metronome are live. Re-renders the
* two short bursts and re-sets the codec volume; a click landing mid-re-render is at
Expand Down
2 changes: 1 addition & 1 deletion KitchenSync/main/usb_midi_host.c
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ uint32_t usb_midi_host_tx(void) { return s_tx; }
* with one in-flight transfer (~1 ms) and the fan-out calling it once per output
* back-to-back inside a 1 ms tick, ~3 of every 4 packets were discarded and only the
* first output ever reached USB gear. Callers must now batch a tick's events into ONE
* transfer (usb_midi_batch) and honour this return value. */
* transfer (usb_midi_batch) and honor this return value. */
bool usb_midi_host_send(const uint8_t* data, int len)
{
if (!s_ready || len <= 0 || len > 64) return false;
Expand Down
4 changes: 2 additions & 2 deletions KitchenSyncTouch/KitchenSyncTouch.ino
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ void setup() {

#ifdef HAS_STATUS_RGB
// ESP-043: drive the Super Mini's onboard WS2812 (GPIO48) as a status light. The
// state->colour decision is the pure, host-tested ktouch_status_rgb(); this seam just
// state->color decision is the pure, host-tested ktouch_status_rgb(); this seam just
// samples the live state and pushes the result to the LED. dark stopped, amber blink armed,
// green beat-flash running, cyan when Link-locked, brighter accent on the bar-1 downbeat.
#include "ktouch_status_rgb.h"
Expand All @@ -202,7 +202,7 @@ static void status_rgb_tick() {
ktouch_midi_locked() != 0,
ktouch_midi_beats(),
g_config.quantum_beats);
// Only touch the RMT when the colour actually changes -- loop() runs every ~5 ms and
// Only touch the RMT when the color actually changes -- loop() runs every ~5 ms and
// rgbLedWrite reconfigures the peripheral on each call.
static StatusRgb last = { 1, 1, 1 }; // impossible first value forces the first write
if (c.r == last.r && c.g == last.g && c.b == last.b) return;
Expand Down
2 changes: 1 addition & 1 deletion KitchenSyncTouch/ktouch_config_migrate.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ bool ktouch_migrate_legacy_config(KsConfig* out, const void* blob, size_t blob_l
return false; // not a legacy Touch version
}

// A migration is not a licence to skip the guard — bit-rot is bit-rot at any version.
// A migration is not a license to skip the guard — bit-rot is bit-rot at any version.
// Same gate a verbatim KsConfig load runs through. On failure, restore clean defaults
// so the caller never sees a half-migrated struct.
if (!ks_config_valid(out)) {
Expand Down
2 changes: 1 addition & 1 deletion KitchenSyncTouch/ktouch_config_nvs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static bool migrate_legacy_keys(KsConfig* cfg) {
prefs.getString("wifi_ssid", cfg->wifi[0].ssid, sizeof(cfg->wifi[0].ssid));
prefs.getString("wifi_pass", cfg->wifi[0].pass, sizeof(cfg->wifi[0].pass));

// A migration is not a licence to skip the guard: garbage in the old keys is still
// A migration is not a license to skip the guard: garbage in the old keys is still
// garbage. Same gate the blob path runs through.
return ks_config_valid(cfg);
}
Expand Down
2 changes: 1 addition & 1 deletion KitchenSyncTouch/ktouch_display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ static bool read_touch(axs_touch_t *t) {
return axs5106l_parse(buf, sizeof(buf), t) == 0;
}

// The whole lower area is one transport toggle; colour + label follow the state.
// The whole lower area is one transport toggle; color + label follow the state.
static void draw_toggle(int state) {
uint32_t bg; const char* big; const char* hint;
if (state == TL_RUNNING) { bg = 0x1f7a0eu; big = "PLAYING"; hint = "tap to STOP"; }
Expand Down
4 changes: 2 additions & 2 deletions KitchenSyncTouch/ktouch_midi_out.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ static MasterClock s_mc; // ESP-027
*
* Worth knowing: this task is pinned to core 1, and Arduino's loopTask runs on core 1
* too (CONFIG_ARDUINO_RUNNING_CORE=1). So the MIDI writer SHARES a core with the web
* server, the LovyanGFX SPI blits and the I2C touch reads. It out-prioritises them
* server, the LovyanGFX SPI blits and the I2C touch reads. It out-prioritizes them
* (6 vs 1), so plain preemption should cover it -- which is exactly why a large gap
* would be interesting: it would mean something preemption cannot fix.
*
Expand Down Expand Up @@ -261,7 +261,7 @@ static void writer_task(void*) {
if (gap > s_max_gap_us) s_max_gap_us = gap;
if (work > s_max_work_us) {
s_max_work_us = work;
// Stage split re-labelled to match the new order -- the probe must keep
// Stage split re-labeled to match the new order -- the probe must keep
// measuring the stage it names, or ESP-018's next diagnosis reads a lie.
w_beats = t_beats - tk0; // tempo_source_beats_now()
w_tport = t_tport - t_beats; // clock_output_step + transport byte + publish
Expand Down
6 changes: 3 additions & 3 deletions KitchenSyncTouch/ktouch_status_rgb.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once
// ESP-043: the headless Super Mini's onboard WS2812 (GPIO48) as a status light. A screenless
// clock box has no other local feedback, so the colour must read at a glance across a room.
// clock box has no other local feedback, so the color must read at a glance across a room.
// This is the STATE -> APPEARANCE decision as a PURE function, host-tested -- it does not live
// in loop() (the same rule that keeps transport/tempo mapping out of the views).
//
Expand All @@ -9,7 +9,7 @@
// waiting for the bar line -- the DeviceDetail armed cadence)
// running, free-run -> green beat flash
// running, Link-locked -> cyan beat flash (phase-locked to a session)
// bar-1 downbeat -> a brighter accent of the running/armed colour
// bar-1 downbeat -> a brighter accent of the running/armed color
//
// The flash is HARD-EDGED (bright for the first slice of each beat, dark the rest), never a
// soft throb -- a throb reads as "loading", and a beat is a hammer-fall.
Expand All @@ -23,7 +23,7 @@ extern "C" {

typedef struct { uint8_t r, g, b; } StatusRgb;

// Map the live state to the LED colour for THIS instant. `beats` is the writer's continuous
// Map the live state to the LED color for THIS instant. `beats` is the writer's continuous
// beat counter (ktouch_midi_beats); `quantum` is beats per bar, for the downbeat accent.
// link_locked is true only when the clock is phase-locked to a Link session.
StatusRgb ktouch_status_rgb(TransportLaunchState transport, bool link_locked,
Expand Down
2 changes: 1 addition & 1 deletion KitchenSyncTouch/ktouch_transport.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ void ktouch_transport_post(TransportLaunchIntent intent);
TransportLaunchIntent ktouch_transport_take(void); // consumes; NONE when empty

// The writer publishes the current launch state (TL_STOPPED/ARMED/RUNNING) each
// tick; the display reads it to colour the toggle and pick the tap direction.
// tick; the display reads it to color the toggle and pick the tap direction.
void ktouch_transport_publish_state(int launch_state);
int ktouch_transport_state(void);

Expand Down
4 changes: 2 additions & 2 deletions KitchenSyncTouch/ktouch_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ static void handle_status() {
* defers to Link.
*
* ks_status.h defines link_owns as a LIVE claim: "when link_owns is
* true the manual PLAY/STOP buttons are ignored, so the UI greys
* true the manual PLAY/STOP buttons are ignored, so the UI grays
* them". A client is entitled to take that literally, and the iOS app
* does -- it stops SENDING the tap.
*
Expand Down Expand Up @@ -456,7 +456,7 @@ static void handle_status() {
/* ESP-030 pt3: POST /live — apply a partial patch IMMEDIATELY, no reboot.
*
* This device had no /live at all. It grew bespoke one-off endpoints (/nudge, /bright)
* while the P4 generalised to /live plus a form grammar. Same drift as /status: the
* while the P4 generalized to /live plus a form grammar. Same drift as /status: the
* clock ENGINE is shared (46 symlinked modules), the WEB layer never was. So every
* live edit the iOS app made — nudge, swing, rate, cable — POSTed to /live, got a 404,
* and was swallowed. The controls looked dead because the request landed nowhere.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ preserved; recovery tag `pre-split-2026-07-04` on the old repo).
(P4-032). Streams the P4's onboard mic into an Ableton Live 12.4 session as a
**Link Audio** channel, sample-rate-adaptive and calibrated to ~1 ms of Live's
grid. Deliberately separate from KitchenSync: it links the **GPLv2** Ableton
Link SDK, which cannot ship in closed hardware without a commercial licence
Link SDK, which cannot ship in closed hardware without a commercial license
from Ableton — a business gate nobody has cleared yet. See
[`LinkAudioPoC/README.md`](LinkAudioPoC/README.md).

Expand Down
Loading
Loading