diff --git a/.american-spelling-allow b/.american-spelling-allow
new file mode 100644
index 0000000..9194f54
--- /dev/null
+++ b/.american-spelling-allow
@@ -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/
diff --git a/AGENTS.md b/AGENTS.md
index 249f2b2..8bc729d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff60649..4ce1760 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/KitchenSync/README.md b/KitchenSync/README.md
index 52efb23..df0183b 100644
--- a/KitchenSync/README.md
+++ b/KitchenSync/README.md
@@ -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.
diff --git a/KitchenSync/main/ks_display.h b/KitchenSync/main/ks_display.h
index bddc44a..c26e3cd 100644
--- a/KitchenSync/main/ks_display.h
+++ b/KitchenSync/main/ks_display.h
@@ -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);
diff --git a/KitchenSync/main/ks_led.h b/KitchenSync/main/ks_led.h
index fe0ec8c..c0e142a 100644
--- a/KitchenSync/main/ks_led.h
+++ b/KitchenSync/main/ks_led.h
@@ -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);
diff --git a/KitchenSync/main/ks_main.c b/KitchenSync/main/ks_main.c
index 90f599b..513b6bc 100644
--- a/KitchenSync/main/ks_main.c
+++ b/KitchenSync/main/ks_main.c
@@ -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.) */
diff --git a/KitchenSync/main/ks_tick.c b/KitchenSync/main/ks_tick.c
index c4f6765..b4dab17 100644
--- a/KitchenSync/main/ks_tick.c
+++ b/KitchenSync/main/ks_tick.c
@@ -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. */
@@ -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).
diff --git a/KitchenSync/main/ks_web.cpp b/KitchenSync/main/ks_web.cpp
index da336aa..4731c4a 100644
--- a/KitchenSync/main/ks_web.cpp
+++ b/KitchenSync/main/ks_web.cpp
@@ -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);
@@ -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}
@@ -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){
@@ -396,7 +396,7 @@ static std::string build_outputs()
+ ">";
// 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("
RUN")
@@ -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()
{
@@ -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.
*
@@ -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;
diff --git a/KitchenSync/main/metronome_audio.c b/KitchenSync/main/metronome_audio.c
index 6fef667..bb7032d 100644
--- a/KitchenSync/main/metronome_audio.c
+++ b/KitchenSync/main/metronome_audio.c
@@ -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
diff --git a/KitchenSync/main/usb_midi_host.c b/KitchenSync/main/usb_midi_host.c
index cce2ff8..e4909d8 100644
--- a/KitchenSync/main/usb_midi_host.c
+++ b/KitchenSync/main/usb_midi_host.c
@@ -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;
diff --git a/KitchenSyncTouch/KitchenSyncTouch.ino b/KitchenSyncTouch/KitchenSyncTouch.ino
index d04a171..55d6535 100644
--- a/KitchenSyncTouch/KitchenSyncTouch.ino
+++ b/KitchenSyncTouch/KitchenSyncTouch.ino
@@ -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"
@@ -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;
diff --git a/KitchenSyncTouch/ktouch_config_migrate.c b/KitchenSyncTouch/ktouch_config_migrate.c
index cdf11b1..530ff90 100644
--- a/KitchenSyncTouch/ktouch_config_migrate.c
+++ b/KitchenSyncTouch/ktouch_config_migrate.c
@@ -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)) {
diff --git a/KitchenSyncTouch/ktouch_config_nvs.cpp b/KitchenSyncTouch/ktouch_config_nvs.cpp
index 40476f4..17c34fe 100644
--- a/KitchenSyncTouch/ktouch_config_nvs.cpp
+++ b/KitchenSyncTouch/ktouch_config_nvs.cpp
@@ -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);
}
diff --git a/KitchenSyncTouch/ktouch_display.cpp b/KitchenSyncTouch/ktouch_display.cpp
index 051ba55..8b97dcb 100644
--- a/KitchenSyncTouch/ktouch_display.cpp
+++ b/KitchenSyncTouch/ktouch_display.cpp
@@ -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"; }
diff --git a/KitchenSyncTouch/ktouch_midi_out.cpp b/KitchenSyncTouch/ktouch_midi_out.cpp
index 99b33e2..9c1d8d9 100644
--- a/KitchenSyncTouch/ktouch_midi_out.cpp
+++ b/KitchenSyncTouch/ktouch_midi_out.cpp
@@ -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.
*
@@ -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
diff --git a/KitchenSyncTouch/ktouch_status_rgb.h b/KitchenSyncTouch/ktouch_status_rgb.h
index 8987c27..5d81dc6 100644
--- a/KitchenSyncTouch/ktouch_status_rgb.h
+++ b/KitchenSyncTouch/ktouch_status_rgb.h
@@ -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).
//
@@ -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.
@@ -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,
diff --git a/KitchenSyncTouch/ktouch_transport.h b/KitchenSyncTouch/ktouch_transport.h
index 5b4e759..c9314ec 100644
--- a/KitchenSyncTouch/ktouch_transport.h
+++ b/KitchenSyncTouch/ktouch_transport.h
@@ -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);
diff --git a/KitchenSyncTouch/ktouch_web.cpp b/KitchenSyncTouch/ktouch_web.cpp
index d11f6ab..677b186 100644
--- a/KitchenSyncTouch/ktouch_web.cpp
+++ b/KitchenSyncTouch/ktouch_web.cpp
@@ -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.
*
@@ -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.
diff --git a/README.md b/README.md
index 179aab9..9f53515 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/X32Link/X32Link.ino b/X32Link/X32Link.ino
index 4e0a604..37febfc 100644
--- a/X32Link/X32Link.ino
+++ b/X32Link/X32Link.ino
@@ -95,12 +95,12 @@ static WifiConnPolicy g_wifi_pol;
#define LED_STATUS_GREEN 0x00FF00 // WiFi-down blink stays this, never user-configurable
#if defined(LED_RGB)
-static uint32_t s_led_rgb = 0; // current flash colour, set per flash (LNK-039)
+static uint32_t s_led_rgb = 0; // current flash color, set per flash (LNK-039)
#endif
-// LNK-039: pick this flash's colour from the configured dot colours —
-// bar-1 accent when bar phase is available, plain beat colour otherwise.
-// Reads g_config at each flash, so /live colour edits apply immediately.
+// LNK-039: pick this flash's color from the configured dot colors —
+// bar-1 accent when bar phase is available, plain beat color otherwise.
+// Reads g_config at each flash, so /live color edits apply immediately.
static void led_pick_color(bool bar_valid) {
#if defined(LED_RGB)
float bar_phase = bar_valid ? tempo_source_phase((float)g_config.quantum_beats) : 0.0f;
@@ -157,7 +157,7 @@ static void led_task(void*) {
if (valid) {
float phase = tempo_source_phase(1.0f);
if (led_phase_should_flash(prev_phase, phase, valid)) {
- led_pick_color(true); // LNK-039: bar-1 accent vs beat colour
+ led_pick_color(true); // LNK-039: bar-1 accent vs beat color
led_set(true);
vTaskDelay(pdMS_TO_TICKS(LED_FLASH_MS));
led_set(false);
@@ -171,7 +171,7 @@ static void led_task(void*) {
// against a stale value (no double-flash/stutter).
prev_phase = -1.0f;
if (tempo_source_beat()) {
- led_pick_color(false); // no bar info in the sync gap: beat colour
+ led_pick_color(false); // no bar info in the sync gap: beat color
led_set(true);
vTaskDelay(pdMS_TO_TICKS(LED_FLASH_MS));
led_set(false);
@@ -191,7 +191,7 @@ static void led_task(void*) {
if (wifi_down_blink_due(now_ms, last_wifi_blink_ms,
WIFI_DOWN_BLINK_INTERVAL_MS, wifi_connected)) {
last_wifi_blink_ms = now_ms;
- // Diagnostic blink keeps a fixed colour: a dark user beat colour
+ // Diagnostic blink keeps a fixed color: a dark user beat color
// (down to #000000) must never hide the "WiFi is down" signal.
#if defined(LED_RGB)
s_led_rgb = led_flash_rgb(LED_STATUS_GREEN, LED_STATUS_GREEN, 0.0f, false, LED_BRIGHTNESS);
diff --git a/X32Link/app_config.c b/X32Link/app_config.c
index 6e109fa..38aa4a1 100644
--- a/X32Link/app_config.c
+++ b/X32Link/app_config.c
@@ -99,7 +99,7 @@ bool config_validate(const AppConfig* cfg) {
}
// ESP-040: an HTML posts "#rrggbb"; accept with or without the '#'.
-// Rejects anything that isn't exactly six hex digits so a garbled colour never reaches
+// Rejects anything that isn't exactly six hex digits so a garbled color never reaches
// the config as a partial parse.
static int parse_hex_color(const char* v, int* out) {
if (v[0] == '#') v++;
@@ -128,7 +128,7 @@ bool app_config_set_kv(AppConfig* cfg, const char* key, const char* val) {
if (strcmp(key, "wifi_pass") == 0) return keep_or_copy(cfg->wifi_pass, sizeof(cfg->wifi_pass), val);
if (strcmp(key, "mixer_ip") == 0) return keep_or_copy(cfg->mixer_ip, sizeof(cfg->mixer_ip), val);
- // Colours: the SHARED key (led_beat/led_accent, what /config.json emits) and the
+ // Colors: the SHARED key (led_beat/led_accent, what /config.json emits) and the
// device's own form key (dot_beat/dot_acc) are aliases for one field.
if (strcmp(key, "led_beat") == 0 || strcmp(key, "dot_beat") == 0) {
int c; if (!parse_hex_color(val, &c)) return false;
@@ -163,7 +163,7 @@ void x32_form_merge(AppConfig* out, const AppConfig* base,
if (full_form) {
// The device's own page posted the whole form, so an unchecked (absent) checkbox
// means "off". Pre-clear the two checkbox-backed booleans; a present key below
- // flips its own back on. Value fields (input_source, colours, model, ...) are
+ // flips its own back on. Value fields (input_source, colors, model, ...) are
// NEVER cleared by absence — only genuine checkboxes are, which is the whole point.
out->midi_clock_out_enable = 0;
out->phase_display_mode = 0;
diff --git a/X32Link/app_config.h b/X32Link/app_config.h
index 4a15bdf..322966f 100644
--- a/X32Link/app_config.h
+++ b/X32Link/app_config.h
@@ -71,7 +71,7 @@ bool app_config_set(AppConfig* cfg, AppConfigField field, int value);
// quantum_beats), plus the X32-only fields (model, mixer_ip, fx_slot, input_source,
// phase_flash). Int fields route through app_config_set (so config_validate stays the one
// range owner); strings are bounded-copied and a BLANK ssid/pass/ip is a no-op "keep
-// current" (a form can't show a password). Returns true iff the key was recognised and
+// current" (a form can't show a password). Returns true iff the key was recognized and
// the value applied.
bool app_config_set_kv(AppConfig* cfg, const char* key, const char* val);
@@ -88,7 +88,7 @@ typedef struct { const char* key; const char* val; } X32FormField;
// full_form == true → the device's OWN page posted the whole form, so an ABSENT
// checkbox-backed boolean (midi_clock_out_enable, phase_display_mode)
// means "off" and is pre-cleared before the present keys reapply.
-// Value fields (input_source, colours, ...) are NEVER zeroed by
+// Value fields (input_source, colors, ...) are NEVER zeroed by
// absence — only genuine checkboxes are.
//
// `out` is not validated here; the caller runs config_validate() exactly as before.
diff --git a/X32Link/app_config_nvs.cpp b/X32Link/app_config_nvs.cpp
index 0a03948..b7e10af 100644
--- a/X32Link/app_config_nvs.cpp
+++ b/X32Link/app_config_nvs.cpp
@@ -47,7 +47,7 @@ static bool migrate_legacy(AppConfig* cfg) {
prefs.getString("wifi_ssid", legacy.wifi_ssid, sizeof(legacy.wifi_ssid));
prefs.getString("wifi_pass", legacy.wifi_pass, sizeof(legacy.wifi_pass));
- // A migration is not a licence to skip the guard: garbage in the old keys is
+ // 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.
if (!config_validate(&legacy)) return false;
diff --git a/X32Link/beat_source.h b/X32Link/beat_source.h
index a928a58..0570a3f 100644
--- a/X32Link/beat_source.h
+++ b/X32Link/beat_source.h
@@ -31,7 +31,7 @@ extern "C" {
// went silent for over TWO MINUTES while /status cheerfully reported sync:1 peers:1.
//
// 0.25 beat = 6 pulses at 24 PPQN. Smaller backsteps (an ordinary xform refinement, sub-ms)
-// cost a few pulses of quantisation and are absorbed; anything past this is a re-origin.
+// cost a few pulses of quantization and are absorbed; anything past this is a re-origin.
#define BEAT_SOURCE_BACKSTEP_BEATS 0.25
typedef struct {
diff --git a/X32Link/beat_synth.h b/X32Link/beat_synth.h
index 78244fd..7c4ee00 100644
--- a/X32Link/beat_synth.h
+++ b/X32Link/beat_synth.h
@@ -16,14 +16,14 @@ typedef struct {
uint32_t last_ms; // wall-clock ms of the last emitted beat
} BeatSynth;
-// Clear state (last_ms = 0). Behaviour then matches a fresh static counter:
+// Clear state (last_ms = 0). Behavior then matches a fresh static counter:
// the first step() with now_ms past one interval fires immediately.
void beat_synth_reset(BeatSynth* s);
// Advance to now_ms and return true if a beat is due at `bpm`. Fires when
// (now_ms - last_ms) has reached the 60000/bpm interval (unsigned/wrap-safe),
// then latches last_ms = now_ms. bpm <= 0 always returns false and leaves state
-// untouched (so a resumed BPM fires promptly, matching the old inline behaviour).
+// untouched (so a resumed BPM fires promptly, matching the old inline behavior).
bool beat_synth_step(BeatSynth* s, uint32_t now_ms, float bpm);
#ifdef __cplusplus
diff --git a/X32Link/clock_ticker.c b/X32Link/clock_ticker.c
index e33b410..86e1011 100644
--- a/X32Link/clock_ticker.c
+++ b/X32Link/clock_ticker.c
@@ -5,7 +5,7 @@
void clock_ticker_reset(ClockTicker* s) {
s->last_tick = 0;
s->primed = false;
- /* Zeroed here so the counter is always INITIALISED -- callers do
+ /* Zeroed here so the counter is always INITIALIZED -- callers do
* `ClockTicker t; clock_ticker_reset(&t);`, so leaving it alone means it starts as
* stack garbage and can never be trusted. reset() only fires at startup and on Link
* phase-invalid (no clock to drop anyway), so nothing real is lost. Accumulating a
diff --git a/X32Link/clock_ticker.h b/X32Link/clock_ticker.h
index 560c591..751125d 100644
--- a/X32Link/clock_ticker.h
+++ b/X32Link/clock_ticker.h
@@ -23,7 +23,7 @@ typedef struct {
// burst -- the right musical call, but `return 0` throws every pending pulse
// away, and until now it did so with NO trace: no burst on the analyzer, no
// counter, no log. A stall long enough to trip it was simply invisible.
- // reset() ZEROES this, so it is always initialised (callers declare the struct on
+ // reset() ZEROES this, so it is always initialized (callers declare the struct on
// the stack and call reset on it). Keeping a LIFETIME total across resets is the
// caller's job -- a pure struct cannot distinguish "first init" from "re-prime".
uint32_t dropped;
diff --git a/X32Link/ks_config.c b/X32Link/ks_config.c
index 8019e4b..e3b31bf 100644
--- a/X32Link/ks_config.c
+++ b/X32Link/ks_config.c
@@ -203,7 +203,7 @@ bool ks_config_set(KsConfig* c, const char* key, const char* value) {
c->led_fade = v;
return true;
}
- // Colours arrive from an HTML as "#rrggbb" (the '#' is
+ // Colors arrive from an HTML as "#rrggbb" (the '#' is
// url-decoded before we see it); accept with or without the leading '#'.
if (strcmp(key, "led_beat") == 0 || strcmp(key, "led_accent") == 0) {
const char* hex = (value[0] == '#') ? value + 1 : value;
@@ -247,7 +247,7 @@ void ks_config_live_safe_copy(KsConfig* dst, const KsConfig* src) {
for (int o = 0; o < KS_CLOCK_OUTPUTS; o++) dst->clock[o] = src->clock[o];
dst->tempo_mbpm = src->tempo_mbpm; /* ESP-037: a set tempo is a LIVE edit */
/* ESP-042: the fleet fields are all live -- no reboot to change quantize, the
- * transport gate, the touch behaviour, or the backlight. */
+ * transport gate, the touch behavior, or the backlight. */
dst->quantum_beats = src->quantum_beats;
dst->transport_enable = src->transport_enable;
dst->play_on_release = src->play_on_release;
@@ -357,7 +357,7 @@ ks_decode_result ks_config_decode(KsConfig* out, const void* blob, size_t blob_l
memcpy(&v1, blob, sizeof(v1));
KsConfig candidate;
migrate_v1(&candidate, &v1);
- // A migration is not a licence to skip the guard.
+ // A migration is not a license to skip the guard.
if (!ks_config_valid(&candidate)) return KS_DECODE_DEFAULTED;
*out = candidate;
return KS_DECODE_MIGRATED;
diff --git a/X32Link/ks_config.h b/X32Link/ks_config.h
index 58bee24..97310f1 100644
--- a/X32Link/ks_config.h
+++ b/X32Link/ks_config.h
@@ -80,8 +80,8 @@ typedef struct {
int led_brightness; // 0..100 — master strip brightness % (P4-019)
int led_mode; // 0..2 — pattern: 0 chase, 1 flash, 2 fill
int led_fade; // 0..100 — dim across a beat % (0 steady .. 100 dark by next)
- int led_beat_color; // 0xRRGGBB — colour for beats 2..N
- int led_accent_color; // 0xRRGGBB — colour for the bar-1 downbeat
+ int led_beat_color; // 0xRRGGBB — color for beats 2..N
+ int led_accent_color; // 0xRRGGBB — color for the bar-1 downbeat
int follow_beat_enable; // 0/1 -- mic-based tempo detection (P4-020, display only)
// ESP-037: settable free-run tempo, milli-BPM (128000 = 128.000 BPM). Drives the
diff --git a/X32Link/ks_config_json.h b/X32Link/ks_config_json.h
index 329aa65..22defa1 100644
--- a/X32Link/ks_config_json.h
+++ b/X32Link/ks_config_json.h
@@ -75,7 +75,7 @@ typedef struct {
//
// `wifi` has caps.wifi_slots entries and `clock` has caps.outputs entries -- the FITTED
// counts, never padded, because a client renders a row/card per element and would
-// otherwise draw slots and outputs the device cannot honour. Same order as the form's
+// otherwise draw slots and outputs the device cannot honor. Same order as the form's
// slot/output numbering (slot 0 is the unsuffixed "wifi_ssid"/"clk0_*" keys).
//
// Returns snprintf()'s return value (same truncation-detection convention as
diff --git a/X32Link/ks_status.h b/X32Link/ks_status.h
index 0753ca1..7b025f6 100644
--- a/X32Link/ks_status.h
+++ b/X32Link/ks_status.h
@@ -31,7 +31,7 @@ extern "C" {
// so the UI can render "starting on next bar..." rather than a dead button.
// `playing` is the session's real transport state and `link_owns` is true once a
// Link peer has published StartStopState (ks_tick arbitration): when link_owns is
-// true the manual PLAY/STOP buttons are ignored, so the UI greys them and shows
+// true the manual PLAY/STOP buttons are ignored, so the UI grays them and shows
// the session's `playing` state instead of the (frozen) manual launch state.
// `tick` is the 1 ms clock task's health (P4-038), emitted as the same
// "drop"/"burst"/"gap"/"work"/"over"/"core"/"w_beats"/"w_clock" block X32Link
diff --git a/X32Link/led_phase.h b/X32Link/led_phase.h
index 497c2ab..7925b7c 100644
--- a/X32Link/led_phase.h
+++ b/X32Link/led_phase.h
@@ -24,11 +24,11 @@ extern "C" {
// decision is fully described by its inputs).
bool led_phase_should_flash(float prev_phase, float phase, bool valid);
-// LNK-039: which colour should this flash be, at what level? Picks the
+// LNK-039: which color should this flash be, at what level? Picks the
// accent for the bar downbeat (int part of `bar_phase` == 0, same rule as
-// the touch dot) when `bar_valid`, else the beat colour; then scales each
+// the touch dot) when `bar_valid`, else the beat color; then scales each
// channel by bright/255 (40 reproduces the old hardcoded NeoPixel level).
-// Colours are 0xRRGGBB, matching dot_beat_color/dot_accent_color.
+// Colors are 0xRRGGBB, matching dot_beat_color/dot_accent_color.
uint32_t led_flash_rgb(uint32_t beat_rgb, uint32_t accent_rgb, float bar_phase,
bool bar_valid, uint8_t bright);
diff --git a/X32Link/metro_strip.h b/X32Link/metro_strip.h
index 8d78817..0321c79 100644
--- a/X32Link/metro_strip.h
+++ b/X32Link/metro_strip.h
@@ -5,7 +5,7 @@
// CHASE — one block per beat walks across the strip (npix/quantum px per beat)
// FLASH — every pixel pulses together on each beat
// FILL — pixels fill across the bar like a progress bar, resetting each downbeat
-// The bar-1 downbeat uses the accent colour, other beats the beat colour. No I/O.
+// The bar-1 downbeat uses the accent color, other beats the beat color. No I/O.
// Host-tested in test/test_metro_strip.c. No Arduino/ESP-IDF dependency.
#include
@@ -23,8 +23,8 @@ extern "C" {
typedef struct { uint8_t r, g, b; } RGB;
typedef struct {
- RGB beat; // colour for beats 2..N
- RGB accent; // colour for the bar-1 downbeat
+ RGB beat; // color for beats 2..N
+ RGB accent; // color for the bar-1 downbeat
uint8_t bright; // master brightness, 0..100 %
uint8_t mode; // METRO_STRIP_CHASE / FLASH / FILL
uint8_t fade; // dim across a beat, 0..100 % (0 = steady, 100 = dark by next beat)
@@ -38,7 +38,7 @@ void metro_strip_render(double beats, int quantum, int npix,
// Standby breath (ESP-009): session joined, transport stopped. `phase` is a
// free-running 0..1 ramp (one full breath per period; the caller picks the
-// rate). Every pixel pulses together in the beat colour, dim and never fully
+// rate). Every pixel pulses together in the beat color, dim and never fully
// dark -- a strip that reaches zero looks like a board that crashed between
// blinks, which is the exact confusion this exists to prevent. Carries no beat
// information: in standby there is no beat.
diff --git a/X32Link/midi_bpm.cpp b/X32Link/midi_bpm.cpp
index e05c9c6..370d1dc 100644
--- a/X32Link/midi_bpm.cpp
+++ b/X32Link/midi_bpm.cpp
@@ -5,7 +5,7 @@
#include
void midi_bpm_init() {
- // nothing to initialise — state lives in midi_clock module
+ // nothing to initialize — state lives in midi_clock module
}
float midi_bpm_update() {
diff --git a/X32Link/midi_clock_out_io.cpp b/X32Link/midi_clock_out_io.cpp
index 67794a0..3318b5a 100644
--- a/X32Link/midi_clock_out_io.cpp
+++ b/X32Link/midi_clock_out_io.cpp
@@ -141,7 +141,7 @@ void midi_clock_out_io_begin(void) {
*
* Core 1 is unchanged, and deliberately: Arduino's loopTask is on core 1 too
* (CONFIG_ARDUINO_RUNNING_CORE=1), so the writer shares a core with the web server.
- * It now out-prioritises everything there. What priority CANNOT fix is a flash-cache
+ * It now out-prioritizes everything there. What priority CANNOT fix is a flash-cache
* stall, which freezes both cores regardless — which is exactly why the config write
* in ARC-022 is debounced and kept on a low-priority task. */
xTaskCreatePinnedToCore(midi_clock_out_task, "midi_clk_out", 2048, NULL, 19, NULL, 1);
diff --git a/X32Link/touch_display.cpp b/X32Link/touch_display.cpp
index 8d2501c..fd70062 100644
--- a/X32Link/touch_display.cpp
+++ b/X32Link/touch_display.cpp
@@ -317,8 +317,8 @@ static void enter_keyboard(void) {
// LNK-036: paint the phase indicator. Called every loop (not the 10 Hz text throttle)
// so the beat flash lands on the beat instead of up to a throttle-length behind it.
-// Mode 0 = sweeping marker; mode 1 = beat-flash dot: light on each beat (accent colour
-// on the bar-1 downbeat, beat colour otherwise) then blank to black between beats so
+// Mode 0 = sweeping marker; mode 1 = beat-flash dot: light on each beat (accent color
+// on the bar-1 downbeat, beat color otherwise) then blank to black between beats so
// each beat reads as a distinct flash.
#define DOT_FLASH_ON_MS 70
static void render_phase_indicator(const TempoSnapshot &ts, uint32_t now) {
diff --git a/X32Link/transport_launch.h b/X32Link/transport_launch.h
index 39f2835..4b4db55 100644
--- a/X32Link/transport_launch.h
+++ b/X32Link/transport_launch.h
@@ -5,7 +5,7 @@
// grid, so gear started from the web UI lands in time with the Link session.
//
// Start is quantized, Stop is immediate. That asymmetry is deliberate: it is
-// Ableton's own launch behaviour and what muscle memory expects.
+// Ableton's own launch behavior and what muscle memory expects.
//
// One instance per clock output (P4-010 gives each output its own division,
// phase and swing; ESP-011 gives each its own transport, so the drum machine
diff --git a/X32Link/web_config.cpp b/X32Link/web_config.cpp
index 7766ab4..b8f3530 100644
--- a/X32Link/web_config.cpp
+++ b/X32Link/web_config.cpp
@@ -22,7 +22,7 @@ extern AppConfig g_config;
extern void config_save(const AppConfig*);
// ARC-022: /live marks this dirty; web_config_handle() writes the blob once the edits
-// settle. Marked rather than written because config is ONE nvs blob -- a colour picker
+// settle. Marked rather than written because config is ONE nvs blob -- a color picker
// dragged across the wheel emits a POST per frame.
static ConfigPersist s_persist;
// Live tempo comes from the tempo_snapshot seam (ARC-001): one atomic read
@@ -128,7 +128,7 @@ form .row:nth-child(2){animation-delay:.10s}form .row:nth-child(3){animation-del
-
beat-flash dot vs sweep wheel · colours also set the beat LED
+
beat-flash dot vs sweep wheel · colors also set the beat LED
BEAT
BAR1
@@ -378,19 +378,19 @@ static void handle_status() {
* led It really does have one (a NeoPixel beat LED). */
static void handle_config_json() {
/* KsCaps.led means an ADDRESSABLE strip -- brightness, mode, fade, beat/accent
- * colour. That is only true on the LED_RGB builds (the QT Py's NeoPixel). The
+ * color. That is only true on the LED_RGB builds (the QT Py's NeoPixel). The
* headless board's LED is a plain on/off pin, and the Waveshare touch board has no
* MCU-controlled LED at all (LED_NONE).
*
* So this is #if'd, not hardcoded true. Claiming `led` on a board with a bare
- * indicator would make a client draw colour pickers and a brightness slider for
- * hardware that cannot honour any of them -- which is the ENTIRE point of caps, and
+ * indicator would make a client draw color pickers and a brightness slider for
+ * hardware that cannot honor any of them -- which is the ENTIRE point of caps, and
* the same lie as reporting a metronome on a board with no speaker.
*
* Capabilities are a property of the BUILD. Solder a WS2812 on and rebuild with
* LED_RGB, and the LED section appears with no client change at all. */
/* ESP-037: the QT Py NeoPixel was the only addressable-strip X32Link and it's gone,
- * so no X32Link build has a controllable colour strip: the Super Mini's LED is a
+ * so no X32Link build has a controllable color strip: the Super Mini's LED is a
* bare on/off pin, the Waveshare has none. led = false. */
static const KsCaps caps = {
.metronome = false, // no speaker
@@ -484,11 +484,11 @@ static void handle_save() {
}
// LNK-037: apply live-safe fields to the running config with no reboot, mirroring the
-// P4's /live. touch_display reads g_config each render, so the phase dot mode/colours
+// P4's /live. touch_display reads g_config each render, so the phase dot mode/colors
// update instantly.
//
// ARC-022: these edits are now KEPT. This handler had the same bug as the P4's and the
-// Touch's -- it applied the value and returned, so a colour or quantum set here looked
+// Touch's -- it applied the value and returned, so a color or quantum set here looked
// right until the next power cycle and then quietly wasn't. It marks the config dirty
// now; web_config_handle() writes the blob once the edits settle.
static void handle_live() {
diff --git a/docs/adr/0008-p4-web-ui-is-the-brand-reference.md b/docs/adr/0008-p4-web-ui-is-the-brand-reference.md
index a915909..1d4e610 100644
--- a/docs/adr/0008-p4-web-ui-is-the-brand-reference.md
+++ b/docs/adr/0008-p4-web-ui-is-the-brand-reference.md
@@ -6,7 +6,7 @@ Status: accepted
## Context
Three firmwares serve a rack-panel web config page and share one design system:
-`ui_chrome.{h,c}` (ARC-017) owns the *look* (colour tokens, panel, screws, tempo
+`ui_chrome.{h,c}` (ARC-017) owns the *look* (color tokens, panel, screws, tempo
glass, switches, Save button) and the client plumbing (`poll()`, `showBpm`,
`postLive`). Each firmware keeps its **own form** — the fields and the
page-specific CSS/markup that extend the chrome — because the feature sets differ
@@ -39,7 +39,7 @@ scaled to its own feature set:
master toggle and reveal when it flips on (`syncSect`).
- **Structured `.fld` fields** with `.pre` prefix labels — never inline `style=`.
- Help text is a **token-driven class** (`.hint` / equivalent), never a
- repurposed `.cap` with inline colour/spacing.
+ repurposed `.cap` with inline color/spacing.
- A **responsive desktop treatment** (two-column form, 4-across status "meter
bridge") when the form is long enough to benefit.
diff --git a/docs/adr/0009-converge-the-clock-box-on-esp-idf.md b/docs/adr/0009-converge-the-clock-box-on-esp-idf.md
index d79f5e0..785cc23 100644
--- a/docs/adr/0009-converge-the-clock-box-on-esp-idf.md
+++ b/docs/adr/0009-converge-the-clock-box-on-esp-idf.md
@@ -46,7 +46,7 @@ a *related* product as if it were the repo.
**4. ESP-024 is a forcing function.** Its thesis is hardware-timed RMT edges and `IRAM_ATTR`
placement. The Arduino core ships a **fixed sdkconfig** — no `menuconfig` — so the knobs that
-decide jitter (IRAM, cache behaviour, interrupt allocation) are not reachable. Doing that work
+decide jitter (IRAM, cache behavior, interrupt allocation) are not reachable. Doing that work
on the framework that hides the controls, and then doing it a second time on the P4, is the
worst of both. The timing work should happen once, on the framework that exposes the controls,
in the writer that ships.
diff --git a/docs/adr/0015-extract-ks-core-in-repo.md b/docs/adr/0015-extract-ks-core-in-repo.md
index 37eb2a3..22a92cd 100644
--- a/docs/adr/0015-extract-ks-core-in-repo.md
+++ b/docs/adr/0015-extract-ks-core-in-repo.md
@@ -91,7 +91,7 @@ justify B's throwaway churn. Either way the destination (`shared/`) and manifest
same; only the timing differs.
**Decision (2026-07-16): Option A.** The `shared/` extraction is bundled with the Touch's
-ESP-IDF convergence (ADR-0009 / ESP-026): when the Touch's 53 symlinks are deleted in favour
+ESP-IDF convergence (ADR-0009 / ESP-026): when the Touch's 53 symlinks are deleted in favor
of ESP-IDF path refs, the shared source relocates to `shared/` in the same change, so the
move is symlink-neutral. This ADR therefore does not schedule a standalone move — it makes
`shared/` the agreed destination and folds the mechanics into ESP-026's landing. The
@@ -164,5 +164,5 @@ migration plan below is the checklist for that combined step.
(and re-pointed Touch symlinks) until the Arduino consumers reach ESP-IDF.
- The separate `ks-core` **repo** still waits for its own trigger; this ADR does not create
one. Each later extraction gets its own ADR when its trigger fires.
-- No behaviour changes: this is a move + re-point, gated on the full suite and every firmware
+- No behavior changes: this is a move + re-point, gated on the full suite and every firmware
target still building.
diff --git a/docs/plans/2026-07-10-kitchensynctouch-sketch-design.md b/docs/plans/2026-07-10-kitchensynctouch-sketch-design.md
index 51b69a4..64de04a 100644
--- a/docs/plans/2026-07-10-kitchensynctouch-sketch-design.md
+++ b/docs/plans/2026-07-10-kitchensynctouch-sketch-design.md
@@ -94,7 +94,7 @@ never on USB-host presence — the RC-505 runs headless.
3. **Trimmed web + captive portal + OTA.** `ktouch_web` (wifi + clock + transport +
quantum; reuses `ui_chrome_css/js` + `ui_result_page/ui_update_page`). AP
`KSTouch-Config`. `/status`, `/update`. Delete Inc1 compile-time creds.
-4. Polish: touch quantum ±, phase-dot colour, mDNS (ESP-014), OTA rollback.
+4. Polish: touch quantum ±, phase-dot color, mDNS (ESP-014), OTA rollback.
## Prereq refactor in X32Link (Inc2)
diff --git a/tasks/ARC-013.md b/tasks/ARC-013.md
index bd0cc2c..f400f6f 100644
--- a/tasks/ARC-013.md
+++ b/tasks/ARC-013.md
@@ -22,7 +22,7 @@ are untestable because they only exist next to `esp_wifi_*` calls.
The *same* policy is hand-reimplemented on the S3 in a different (blocking) shape
(`X32Link/X32Link.ino:181-233`), with the 30 s budget as a raw `> 30000` literal.
`wifi_fallback.h:16` even documents "must stay in sync by hand." The "retry forever
-after first IP" rule has no S3 analogue — behaviour already differs.
+after first IP" rule has no S3 analogue — behavior already differs.
Deletion test: delete the 4 statics and nothing reconstructs the policy — it's a state
machine that wants to be a module, not scattered flags.
@@ -40,6 +40,6 @@ rule (subsumes `wifi_fallback`).
- Host-test the full transition table (`test/test_wifi_conn_policy.c`); the fallback
test already proves the pattern.
-- Migrate both targets; removes the "keep in sync by hand" seam. Behaviour on hardware
+- Migrate both targets; removes the "keep in sync by hand" seam. Behavior on hardware
should be unchanged — smoke-test STA connect, mid-session drop (retry-forever), and
first-boot/no-AP fallback on the P4; the blocking connect + AP fallback on the S3.
diff --git a/tasks/ARC-014.md b/tasks/ARC-014.md
index 4a30bed..b8b30ed 100644
--- a/tasks/ARC-014.md
+++ b/tasks/ARC-014.md
@@ -34,5 +34,5 @@ concentrates complexity, doesn't just move it.
- The pump can be host-tested against a fake ops (in-memory socket) — the poll
sequence becomes testable for the first time.
-- No behaviour change intended; diff the two current polls first to confirm they're
+- No behavior change intended; diff the two current polls first to confirm they're
byte-equivalent before collapsing (they should be after this session's changes).
diff --git a/tasks/ARC-015.md b/tasks/ARC-015.md
index 735c843..343cc85 100644
--- a/tasks/ARC-015.md
+++ b/tasks/ARC-015.md
@@ -38,5 +38,5 @@ gating become host-testable; the loop drops to thin I/O.
- Sequence with ARC-016 (live-config snapshot) — `ks_tick_step` should take an
atomically-snapshotted cfg, which is cleaner once the seqlock exists.
-- `test/test_ks_tick.c` covers the fold + fan-out plan. No behaviour change intended;
+- `test/test_ks_tick.c` covers the fold + fan-out plan. No behavior change intended;
smoke-test on hardware (LEDs, sound, MIDI out) after.
diff --git a/tasks/ARC-017.md b/tasks/ARC-017.md
index f5fe5c0..5df33d5 100644
--- a/tasks/ARC-017.md
+++ b/tasks/ARC-017.md
@@ -155,7 +155,7 @@ assembly drops the largest heap allocation on the S3's web path entirely.
- 2026-07-08 — Filed after an audit of the Arduino/ESP-IDF split. The pure-C
sharing (1,024 lines compiled into both via `X32LINK_DIR`) is holding; the web
chrome is the one place ADR-0003's discipline was never applied, and the
- `--mut` colour drift is the first symptom.
+ `--mut` color drift is the first symptom.
- 2026-07-09 — Repointed the sharing mechanism. The ticket originally said
`ui_chrome.c` would reach the second Arduino sketch by symlink (ADR-0003 /
ADR-0006); `X32MidiClock/` is being deleted, so CMake `SRCS` is now the only
@@ -191,7 +191,7 @@ copies that had already drifted unnoticed.
| `.unit{animation:rise}` | S3 only | S3's own block | an entry animation, not chrome |
So the P4 *gains* the switch glow it had lost, and the S3's labels get the lighter
-grey. Both were verified as deliberate, not assumed.
+gray. Both were verified as deliberate, not assumed.
**Chunked assembly.** Neither page copies itself any more. `handle_root()` /
`root_handler()` send the head, then `ui_chrome_css()` straight from flash, then
diff --git a/tasks/ARC-018.md b/tasks/ARC-018.md
index 8396e77..625c05a 100644
--- a/tasks/ARC-018.md
+++ b/tasks/ARC-018.md
@@ -46,7 +46,7 @@ forked instead of joining that mechanism — an oversight, not a decision.
Compile the single X32Link source into KitchenSyncTouch the same way the Link
stack already arrives — committed relative symlinks (mode 120000) into
`../X32Link/`, or the arduino-cli sketch build's existing shared-path scheme —
-and delete the forked copies. No behaviour change: the bytes are identical today,
+and delete the forked copies. No behavior change: the bytes are identical today,
so this is a build-graph change plus five deletions.
Confirm first that no copy carries a Touch-only edit (grep + `diff` in the
diff --git a/tasks/ARC-020.md b/tasks/ARC-020.md
index 3e2b602..6264155 100644
--- a/tasks/ARC-020.md
+++ b/tasks/ARC-020.md
@@ -36,14 +36,14 @@ Touch and X32 **hand-serialize every field into NVS twice**. The gap is stark:
test can exist on host** because `.cpp` NVS glue is excluded from the test
harness and there is no Preferences fake (grep: no fake/stub/mock anywhere).
-**Deletion test:** the persistence behaviour must stay, but the field-by-field
+**Deletion test:** the persistence behavior must stay, but the field-by-field
enumeration inside `app_config_nvs.cpp` is removable duplication — KitchenSync
proves the same job needs zero per-field NVS lines.
## Solution
Adopt KitchenSync's model on Touch and X32: save the config struct as one
-versioned blob, and move all accept/reject/migrate judgement into a pure
+versioned blob, and move all accept/reject/migrate judgment into a pure
`config_decode(blob, len, ver)` compiled into the host suite. The `.cpp` NVS glue
shrinks to blob get/set + commit and "does no judging of its own". Add the
`_Static_assert(sizeof(...) == N)` guard so a layout change is a compile error.
@@ -108,7 +108,7 @@ defaults-reset*, because Dan's Touch (ESP-021) OTAs across this change and a sil
wipe of his WiFi creds and nudge trim is unrecoverable in the field. So
`config_load` reads the blob; if there is no blob but the **old field-keys** are
present, it reads them once, runs them through `config_validate` (a migration is not
-a licence to skip the guard), and writes the blob back. Next boot takes the blob path
+a license to skip the guard), and writes the blob back. Next boot takes the blob path
and the legacy keys are never read again. A fresh flash -- no blob, no legacy keys --
just gets defaults.
diff --git a/tasks/ARC-022.md b/tasks/ARC-022.md
index 2e7dd87..41be38a 100644
--- a/tasks/ARC-022.md
+++ b/tasks/ARC-022.md
@@ -88,7 +88,7 @@ so it is the part that gets tested.
1. As a performer, I want the outputs I set to *not* follow Link to still be set that way
after a power cycle, so that my rig comes up the way I left it.
-2. As a performer, I want per-output swing to survive a reboot, so that a groove I dialled in
+2. As a performer, I want per-output swing to survive a reboot, so that a groove I dialed in
at soundcheck is still there at the downbeat.
3. As a performer, I want per-output phase nudge to survive a reboot, so that I do not
re-align every device against every other device at every gig.
@@ -102,7 +102,7 @@ so it is the part that gets tested.
8. As a performer, I want metronome volume and accent to survive a reboot.
9. As a performer, I want LED brightness to survive a reboot, so that a unit I dimmed for a
dark stage does not come back up at full brightness in front of an audience.
-10. As a performer, I want LED mode, fade, and beat/accent colours to survive a reboot.
+10. As a performer, I want LED mode, fade, and beat/accent colors to survive a reboot.
11. As a KitchenSync Touch owner, I want my nudge setting to survive a reboot, so that the
device stays aligned to the gear it drives.
12. As a KitchenSync Touch owner, I want my screen brightness to survive a reboot, so that a
@@ -125,7 +125,7 @@ so it is the part that gets tested.
20. As a developer, I want one persistence policy shared by both firmwares, so that fixing it
on one device cannot leave the other broken.
21. As a developer, I want the "should this write now?" decision to be pure and host-tested,
- so that its timing behaviour is provable without a bench, a device, or a power cycle.
+ so that its timing behavior is provable without a bench, a device, or a power cycle.
22. As a developer adding a new live-editable field, I want it persisted automatically by
virtue of being live-safe, so that the next field added cannot silently reintroduce this
exact bug.
@@ -215,15 +215,15 @@ applied-but-not-saved is zero (story 18).
## Testing Decisions
-**A good test here asserts observable behaviour through the public interface, and would survive
+**A good test here asserts observable behavior through the public interface, and would survive
a rewrite of the internals.** It says *"a burst of edits produces one write, after the edits
stop"* -- not *"`dirty` was set to true."* The debounce module's public surface is
`mark`/`due`/`reset` and a clock the test controls, which is the whole point of making time an
-input rather than something the module reads: the timing behaviour is provable with no device,
+input rather than something the module reads: the timing behavior is provable with no device,
no flash, and no power cycle.
**Module under test:** the new pure config-persist policy in the shared layer. Host-tested,
-under `test/`, alongside the existing pure suites. Behaviours:
+under `test/`, alongside the existing pure suites. Behaviors:
- A single edit does not write immediately; it writes once the quiet window elapses.
- A burst of edits within the quiet window produces **exactly one** write, after the last one
diff --git a/tasks/ARC-025.md b/tasks/ARC-025.md
index ea9c8ba..fd7c87f 100644
--- a/tasks/ARC-025.md
+++ b/tasks/ARC-025.md
@@ -24,7 +24,7 @@ What is left is not a bug. It is the architecture:
| KitchenSync Touch / X32Link (S3) | **~500 µs** |
All three firmwares generate MIDI clock from a **1 ms FreeRTOS task** (`vTaskDelayUntil`, tick
-rate 1000 Hz). A pulse can only be emitted on a tick boundary. **A 1 ms quantisation grid cannot
+rate 1000 Hz). A pulse can only be emitted on a tick boundary. **A 1 ms quantization grid cannot
produce sub-tick placement**, so ~300-500 µs of jitter is not something we are doing wrong -- it
is the direct, arithmetic consequence of the mechanism.
@@ -67,11 +67,11 @@ punish a guess. Before writing the timer path:
- [ ] Establish the current baseline on the analyzer, at a known BPM, under load. (The rig exists:
P4 DIN mirror on GPIO20, downbeat strobe on GPIO21; MIDI jack **pin 5 = data, pin 2 =
ground** -- pin 4 is +5 V and reads flat high, which has already cost one capture.)
-- [ ] Confirm the hypothesis directly: **is the jitter distribution quantised to ~1 ms?** If the
+- [ ] Confirm the hypothesis directly: **is the jitter distribution quantized to ~1 ms?** If the
pulses are not landing on tick boundaries, the premise is wrong and this ticket should be
closed rather than built. Do not skip this step. The ESP-018 ticket shipped with a **wrong
premise** about core assignment and only measurement caught it.
-- [ ] Then prototype on **one** device, one output, DIN only. Measure. Only generalise if the
+- [ ] Then prototype on **one** device, one output, DIN only. Measure. Only generalize if the
number actually moves.
## The question nobody has asked yet
@@ -82,7 +82,7 @@ the skew-compensation feature was **killed rather than built**. That was a good
measurement deleted a feature.
The same discipline applies here. If a hardware timer buys 331 µs → 20 µs and no listener, no
-sequencer, and no analyzer-visible artefact changes, then this is an engineering vanity metric and
+sequencer, and no analyzer-visible artifact changes, then this is an engineering vanity metric and
should be closed as such.
**But we do not know**, and the honest reason to build it is that it is the last structural
diff --git a/tasks/CLK-001.md b/tasks/CLK-001.md
index ca2e91f..514b863 100644
--- a/tasks/CLK-001.md
+++ b/tasks/CLK-001.md
@@ -20,7 +20,7 @@ authoritative tempo source and everything else locks to it.
Today the family is a *follower-and-bridge*: it receives a Link session and
re-emits MIDI clock (and, on the roadmap, analog). The north star is a
-*clock hub* — put it at the centre of a rig and every other box, modular or
+*clock hub* — put it at the center of a rig and every other box, modular or
software, syncs to it, whether the tempo originates from Link, from an external
clock, from audio, or from the device's own internal/tap clock.
diff --git a/tasks/ESP-011.md b/tasks/ESP-011.md
index 2b3a51e..f6bbbcf 100644
--- a/tasks/ESP-011.md
+++ b/tasks/ESP-011.md
@@ -94,7 +94,7 @@ want it. Options, cheapest first:
1. **Manual only when Link isn't driving.** If no StartStopState has been seen
(`link_proto_start_stop_seen() == false`), the web buttons own transport.
- Once a peer publishes transport, Link wins and the buttons grey out. Simple,
+ Once a peer publishes transport, Link wins and the buttons gray out. Simple,
no conflict, matches how the metronome already behaves.
2. **Manual always overrides**, until the next Link transport edge. More
flexible, but two masters -- the classic source of "why did my gear stop".
diff --git a/tasks/ESP-014.md b/tasks/ESP-014.md
index 12ba47e..84b3d2c 100644
--- a/tasks/ESP-014.md
+++ b/tasks/ESP-014.md
@@ -45,8 +45,8 @@ Two differences from ESP-012 worth knowing before starting:
discoverable via the `_http._tcp` service anyway.
The name derivation is the same shape as `ks_hostname.{h,c}` (pure,
-host-tested). Either generalise that module to take a prefix, or copy it --
-ARC-005/ARC-010 would prefer generalising, and it is a two-line change.
+host-tested). Either generalize that module to take a prefix, or copy it --
+ARC-005/ARC-010 would prefer generalizing, and it is a two-line change.
## Scope
@@ -80,4 +80,4 @@ ARC-005/ARC-010 would prefer generalising, and it is a two-line change.
> *This was generated by AI during triage.*
-**`ready-for-agent`.** Small, fully specified. Generalise `ks_hostname` to take a prefix (two-line change, the ticket's preferred option) rather than copying. Check `ESPmDNS` flash cost against the ~81%-full 1.25MB sketch partition BEFORE wiring it in — if it does not fit, stop and report; do not squeeze. Hardware ACs (.local resolution, two-unit collision): NOT VERIFIED convention.
+**`ready-for-agent`.** Small, fully specified. Generalize `ks_hostname` to take a prefix (two-line change, the ticket's preferred option) rather than copying. Check `ESPmDNS` flash cost against the ~81%-full 1.25MB sketch partition BEFORE wiring it in — if it does not fit, stop and report; do not squeeze. Hardware ACs (.local resolution, two-unit collision): NOT VERIFIED convention.
diff --git a/tasks/ESP-016.md b/tasks/ESP-016.md
index 8bdbc41..e7598c9 100644
--- a/tasks/ESP-016.md
+++ b/tasks/ESP-016.md
@@ -163,7 +163,7 @@ Firmware Engineer).
Increments: (1) boots + Link + DIN/USB clock + tempo display; (2) transport touch
screen (PLAY/STOP -> bar-quantized 0xFA/0xFC); (3) trimmed web + captive portal +
-OTA; (4) polish (mDNS/ESP-014, quantum, colours).
+OTA; (4) polish (mDNS/ESP-014, quantum, colors).
Bench findings so far (2026-07-10): flashed the S3-Touch; the blank screen was a
build-target bug (build_opt.h was `-DBOARD_QTPY_ESP32S3`, no display) -- fixed by
@@ -213,9 +213,9 @@ survives the boot (no composite drop).
Two states (play/stop) = ONE full-screen toggle, not two buttons. Tap anywhere:
STOPPED -> ARMING (amber, next bar) -> PLAYING (green); tap again -> STOPPED. One
-huge target (fat-finger-proof), the screen colour IS the state, and it moots any
+huge target (fat-finger-proof), the screen color IS the state, and it moots any
left/right touch-mirror calibration. Pure logic is `ktouch_toggle_intent(state)`
-(host-tested); the writer publishes launch state for the display to colour + toggle.
+(host-tested); the writer publishes launch state for the display to color + toggle.
**Trigger mode (web setting, Inc3):** `play_on_release` -- fire the toggle on
touch-down (digital-DJ) or on release (turntable-DJ: hold, release to let the beat
diff --git a/tasks/ESP-018.md b/tasks/ESP-018.md
index 5128527..a94580f 100644
--- a/tasks/ESP-018.md
+++ b/tasks/ESP-018.md
@@ -72,7 +72,7 @@ overrun, and the log delayed the next tick, which logged again -- 3945 phantom
- [ ] Tick-health probe in the Touch writer task, reported via `/status`
- [ ] Root cause named with a measurement, not a hypothesis
- [ ] Analyzer capture on GPIO11 over >=30s under web load: **0 stalls, 0 bursts**
-- [ ] Clock jitter characterised. Today the Touch is **533 us stdev (min 15.4 / max
+- [ ] Clock jitter characterized. Today the Touch is **533 us stdev (min 15.4 / max
24.4 ms)** vs the P4's **331 us (+-1 ms)** -- the S3 is measurably looser and we
do not yet know why. Decide whether that is acceptable for the RC-505 or needs
fixing too.
diff --git a/tasks/ESP-019.md b/tasks/ESP-019.md
index 0fe71a6..ad5f846 100644
--- a/tasks/ESP-019.md
+++ b/tasks/ESP-019.md
@@ -21,9 +21,9 @@ This is a performance tool. A mouse hunting for a toggle mid-set is the wrong in
## Why number keys
Direct: one key = one output, no modifier, no chord, no mode. It maps to how the outputs
-are already labelled on screen (Clock Out **1**..**4**), so there is nothing new to learn.
+are already labeled on screen (Clock Out **1**..**4**), so there is nothing new to learn.
-## Behaviour
+## Behavior
- **`1`..`4`** -> toggle that output's transport. Identical to clicking its toggle:
stopped -> PLAY (arms, fires on the bar line); armed/playing -> STOP (immediate).
@@ -67,7 +67,7 @@ same key map.
## Acceptance criteria
- [x] `1`..`4` toggle Clock Out 1..4; `space` toggles all
-- [x] Key and click go through the SAME path (`/transport?out=N&play=…`) -- one behaviour,
+- [x] Key and click go through the SAME path (`/transport?out=N&play=…`) -- one behavior,
not two implementations that can drift
- [x] A key is INERT while focus is in any text input / select / textarea
- [x] A key on a Link-owned output does not act, and SAYS why (does not fail silently)
@@ -140,7 +140,7 @@ bar strobe: 5 rising edges, period mean 1.9980 s (expect 2.0000 s @120 BPM 4/4)
DIN : 0xFA at 2.667299 s | bar line 2.666961 s -> START +0.338 ms from the bar line
USBA : 0xFA at 2.667487 s | bar line 2.666961 s -> START +0.526 ms from the bar line
USB-A lags the DIN wire by +0.188 ms (USB frame scheduling)
-next bar is 1999.7 ms later -- it landed on THIS bar, not a neighbouring one
+next bar is 1999.7 ms later -- it landed on THIS bar, not a neighboring one
```
**+0.338 ms is not merely "small", it is the structurally correct value.** The strobe rises on
diff --git a/tasks/ESP-022.md b/tasks/ESP-022.md
index d7f09fb..14936d1 100644
--- a/tasks/ESP-022.md
+++ b/tasks/ESP-022.md
@@ -25,7 +25,7 @@ no white), ~850 mA budget, ~43 fps. That is the reference target.
## Why
- The LED visual-metronome logic already exists on the P4 (P4-018 bar-position
- chase, P4-019 web customization, LNK-039 dot colours). This ticket productizes
+ chase, P4-019 web customization, LNK-039 dot colors). This ticket productizes
it as a **standalone ESP32 node** rather than a P4 peripheral.
- It's the visual output of **CLK-001** (the universal clock hub): "follow Link →
paint the beat" on hardware that costs a few dollars.
@@ -36,7 +36,7 @@ no white), ~850 mA budget, ~43 fps. That is the reference target.
- **Link receive** — `link_protocol.c` (join session, tempo/phase). Portable.
- **Beat/phase → LED** — the visual-metronome logic from P4-018/019 + the phase
- math (`led_phase`, dot colours LNK-039). Pure, host-tested.
+ math (`led_phase`, dot colors LNK-039). Pure, host-tested.
- **WS2812 driver** — RMT-based NeoPixel output already used on the QT Py path.
## What is new (the lift)
@@ -54,7 +54,7 @@ no white), ~850 mA budget, ~43 fps. That is the reference target.
flash over the air: WLED `/update` (upload `.bin`) or ArduinoOTA/`espota`
(port 3232). Produce an OTA-compatible image + document the one-command push.
4. **Minimal config surface.** Reuse `ui_chrome` for a small web page (WiFi, LED
- count/pin, brightness cap, pattern/colour) — per ADR-0008 mirror the P4 look.
+ count/pin, brightness cap, pattern/color) — per ADR-0008 mirror the P4 look.
## Acceptance Criteria
diff --git a/tasks/ESP-024.md b/tasks/ESP-024.md
index 2b4ea59..c067b76 100644
--- a/tasks/ESP-024.md
+++ b/tasks/ESP-024.md
@@ -30,7 +30,7 @@ spread 1124.0 us peak-to-peak
```
**The spread is one millisecond.** That is the 1 ms writer task rendered visible on the wire —
-the edge can only land on a tick boundary, so the jitter IS the tick quantisation. Every jitter
+the edge can only land on a tick boundary, so the jitter IS the tick quantization. Every jitter
figure previously in this repo was the task's own view of itself; this is the first taken from
the DIN wire, and it confirms the thesis below outright.
@@ -76,13 +76,13 @@ exactly one embedded platform layer and it is ESP32. Do not trade that away on a
## The ceiling to keep straight
-DIN MIDI is 31250 baud, so a clock byte takes **320 us to serialise**. Two things follow,
+DIN MIDI is 31250 baud, so a clock byte takes **320 us to serialize**. Two things follow,
and they are easy to conflate:
- That 320 us is **fixed latency, not jitter.** It is constant, so it does not widen the
distribution. The receiver clocks off the **start bit** — so start-bit jitter below
320 us is still real and still worth removing.
-- It becomes a **quantiser** the moment a clock byte queues behind other MIDI traffic in
+- It becomes a **quantizer** the moment a clock byte queues behind other MIDI traffic in
the UART FIFO. Then the clock byte is delayed by up to a full byte time, per byte
ahead of it. That is an architecture fix (clock bytes jump the queue), not a chip fix.
@@ -118,7 +118,7 @@ RMT cannot drive a UART, so the DIN path needs a different fix:
- Hand the `0xF8` to the UART from a **GPTimer ISR marked `IRAM_ATTR`**, so a flash cache
miss can never delay the edge. ISR latency is a few us and deterministic.
- **Give clock bytes priority over other MIDI traffic** in the FIFO — this is the
- quantiser named above, and it is invisible on an idle wire but ugly under real traffic.
+ quantizer named above, and it is invisible on an idle wire but ugly under real traffic.
If that is not enough, the escalation is to **encode the MIDI frame as an RMT waveform**:
a UART byte is just 10 bit-times of 32 us, which RMT can emit with ~50 ns placement,
@@ -183,7 +183,7 @@ it does not prove the RC-505 (Dan's actual box, which we also do not own) agrees
- [ ] Baseline re-taken on the current build with `linkcli` in-session and the tempo read
from `/status` (not hardcoded)
- [ ] Analog trigger edges generated by RMT; measured on the analyzer
-- [ ] DIN byte handed off from an `IRAM_ATTR` hardware-timer ISR; clock bytes prioritised
+- [ ] DIN byte handed off from an `IRAM_ATTR` hardware-timer ISR; clock bytes prioritized
in the UART FIFO; measured on the analyzer **and** at the ER-1's audio out
- [ ] Before/after table: stdev, p99, min/max, stalls, dropped — idle **and** under web
load, >= 10 min each
diff --git a/tasks/ESP-026.md b/tasks/ESP-026.md
index e1ebab0..eeb8705 100644
--- a/tasks/ESP-026.md
+++ b/tasks/ESP-026.md
@@ -84,7 +84,7 @@ See [ADR-0009](../docs/adr/0009-converge-the-clock-box-on-esp-idf.md).
ESP-024's whole thesis is **hardware-timed RMT edges and `IRAM_ATTR` placement**. The
Arduino core ships a **fixed sdkconfig** — there is no `menuconfig` — so the knobs that
-actually decide jitter (IRAM, cache behaviour, interrupt allocation) are not reachable
+actually decide jitter (IRAM, cache behavior, interrupt allocation) are not reachable
from it.
Doing that timing work on the framework that hides the controls, and then doing it a
diff --git a/tasks/ESP-030.md b/tasks/ESP-030.md
index 627e608..027368c 100644
--- a/tasks/ESP-030.md
+++ b/tasks/ESP-030.md
@@ -24,7 +24,7 @@ Delivered in two parts and fully verified:
passwords as `pass_set` only, and capability flags so absent hardware is never faked.
- **The storage + data model** converged in **ESP-042** (PR #130): the Touch adopted
`KsConfig`, `AppConfig` was retired, and the migration-first hazard this ticket flags in
- bold was honoured exactly — the AppConfig→KsConfig migration was written and host-tested
+ bold was honored exactly — the AppConfig→KsConfig migration was written and host-tested
FIRST (`test_ktouch_migrate`), and the bench Super Mini kept its WiFi (`Skeyelab`) + a
non-default 143.000 BPM across a real upgrade (confirmed by NVS dump). `quantum_beats`
was promoted to the shared schema per item 4.
@@ -91,8 +91,8 @@ unit that is an annoyance. On a unit at a venue it is a dead box.
3. **Touch adopts `KsConfig`**, sharing `ks_config.c` / `ks_config_json.c` / `ks_form.c` the way it
already shares 46 other modules. Its web form and display code move to the shared model.
4. **Find a home for the fields that are genuinely device-specific.** `brightness` is an LCD the P4
- does not have; `play_on_release` is a touch-screen behaviour. `quantum_beats` is arguably useful
- on BOTH (launch quantisation is a Link concept, not a screen concept) — consider promoting it to
+ does not have; `play_on_release` is a touch-screen behavior. `quantum_beats` is arguably useful
+ on BOTH (launch quantization is a Link concept, not a screen concept) — consider promoting it to
the shared schema rather than treating it as Touch-only.
5. **Hardware a device does not have still must not be faked.** If Touch carries `metronome_enable`
in a shared struct, the DEVICE must not claim it can click, and the app must not draw a control
diff --git a/tasks/ESP-034.md b/tasks/ESP-034.md
index ebfb93f..9c9af1b 100644
--- a/tasks/ESP-034.md
+++ b/tasks/ESP-034.md
@@ -23,7 +23,7 @@ reading the source and calling it verified. That is exactly how both bugs shippe
`ks_status.h` defines it as one: *"link_owns is true once a Link peer has published
StartStopState ... when link_owns is true the manual PLAY/STOP buttons are ignored, so
-the UI greys them."* The iOS app takes that literally and **stops sending the tap**.
+the UI grays them."* The iOS app takes that literally and **stops sending the tap**.
The Touch was passing `link_proto_start_stop_seen()` — a STICKY flag ("has any peer EVER
published a StartStopState", latched until the last peer leaves) — while
@@ -43,7 +43,7 @@ it never knew to send.
On the Touch this was not hypothetical: a POST carrying only `wifi`/`clk0_*` set
`clock_enable` AND `transport_enable` to 0, killing play/stop on the touchscreen. And the
iOS app's `saveFormFields` does not emit every P4 checkbox either — if `ks_web.cpp`'s save
-handler has the same absent-means-off behaviour, **saving WiFi from the app will disable
+handler has the same absent-means-off behavior, **saving WiFi from the app will disable
whatever the app has never heard of.**
The Touch's fix: absent = off applies ONLY when the sender declares the whole form
diff --git a/tasks/ESP-039.md b/tasks/ESP-039.md
index 5017e71..ea61142 100644
--- a/tasks/ESP-039.md
+++ b/tasks/ESP-039.md
@@ -30,7 +30,7 @@ files, and half of it has no owner and no test.
`target=CONFIG_IDF_TARGET` identical at every site, only `model` varying. It has **zero test
coverage** anywhere.
-`ESP-014` already names the first half of the fix: *"Generalise `ks_hostname` to take a prefix
+`ESP-014` already names the first half of the fix: *"Generalize `ks_hostname` to take a prefix
… rather than copying."* This ticket does that and folds in the TXT tuple.
This is the canonical shallow-duplication case: adding one TXT key (as ESP-031 did) means editing
@@ -45,7 +45,7 @@ sketch root (ADR-0006/0007), symlinked into `KitchenSyncTouch/`, compiled-by-pat
```c
// device_identity.h
typedef struct { const char* dev; const char* model; const char* target; const char* fw; } DeviceIdentity;
-void device_hostname(const char* prefix, const uint8_t mac[6], char* out, size_t cap); // generalises ks_hostname
+void device_hostname(const char* prefix, const uint8_t mac[6], char* out, size_t cap); // generalizes ks_hostname
// (advertise the TXT tuple via one helper so the key set lives in ONE place)
```
diff --git a/tasks/ESP-040.md b/tasks/ESP-040.md
index 11a8c6b..eb52ea6 100644
--- a/tasks/ESP-040.md
+++ b/tasks/ESP-040.md
@@ -70,7 +70,7 @@ ONE form discipline without X32Link adopting `KsConfig`. `handle_live` was alrea
## Progress
-- `app_config.c`: `app_config_set_kv` (shared + alias keys, blank-keeps-current strings, colours) and
+- `app_config.c`: `app_config_set_kv` (shared + alias keys, blank-keeps-current strings, colors) and
`x32_form_merge` (patch vs full-form pre-clear of the two checkboxes; applies `model` before
`fx_slot` so the slot validates against the right max). 8 new host tests.
- `web_config.cpp`: `handle_save` routes through `merge_post_into` → `x32_form_merge`; the form gained
diff --git a/tasks/ESP-043.md b/tasks/ESP-043.md
index d4c6d93..83864dd 100644
--- a/tasks/ESP-043.md
+++ b/tasks/ESP-043.md
@@ -30,7 +30,7 @@ DevKit's lit buttons). This adds the onboard single RGB as a distinct capability
- `config.h`: `HAS_STATUS_RGB` + `STATUS_RGB_GPIO 48`, gated to `BOARD_S3_SUPERMINI` only.
NOT the Waveshare (GPIO48 is the panel's there) or the DevKit.
-- `ktouch_status_rgb.{c,h}`: the STATE -> COLOUR decision as a PURE, host-tested function —
+- `ktouch_status_rgb.{c,h}`: the STATE -> COLOR decision as a PURE, host-tested function —
it does not live in `loop()` (same rule that keeps tempo/transport mapping out of views):
- stopped -> dark (the light reflects transport; a stopped box says nothing even while
its clock free-runs)
@@ -44,12 +44,12 @@ DevKit's lit buttons). This adds the onboard single RGB as a distinct capability
not a "loading" throb
- `.ino`: a thin `status_rgb_tick()` seam samples `ktouch_transport_state()` /
`ktouch_midi_locked()` / `ktouch_midi_beats()` / `quantum_beats`, calls the pure function,
- and writes the LED only when the colour changes (loop runs every ~5 ms; `rgbLedWrite`
+ and writes the LED only when the color changes (loop runs every ~5 ms; `rgbLedWrite`
reconfigures RMT each call).
## Acceptance criteria
-- [x] The state->colour mapping is a pure, host-tested function (`test_status_rgb.c`, 5 cases:
+- [x] The state->color mapping is a pure, host-tested function (`test_status_rgb.c`, 5 cases:
stopped-dark, flash-on/off, green-vs-cyan, downbeat brighter, armed amber).
- [x] Gated to the Super Mini build; the Waveshare and DevKit builds are unaffected.
- [x] Verified on the bench Super Mini (OTA): amber while armed, cyan beat-flash while
@@ -60,7 +60,7 @@ DevKit's lit buttons). This adds the onboard single RGB as a distinct capability
The operator has purchased **buttons with an RGB LED inside each**. The Super Mini's transport
surface is coming (config.h already flags `HAS_BUTTONS` as expected). Those per-button RGBs
-should reuse THIS colour vocabulary (`ktouch_status_rgb` or a sibling) so the onboard light
-and the button lights say the same thing — one state->colour owner across the box. New ticket
+should reuse THIS color vocabulary (`ktouch_status_rgb` or a sibling) so the onboard light
+and the button lights say the same thing — one state->color owner across the box. New ticket
when the buttons are wired. See also `transport_led.c` (the DevKit's on/off button lamps),
which the RGB buttons would supersede.
diff --git a/tasks/ESP-044.md b/tasks/ESP-044.md
index ccbe17c..7992a29 100644
--- a/tasks/ESP-044.md
+++ b/tasks/ESP-044.md
@@ -57,7 +57,7 @@ stronger, always-awake beacon), so the fix is unconditional rather than board-ga
- Reflashed the bench Super Mini (MAC `3c:0f:02:e2:b0:d4`), NVS erased + compile-time WiFi seed
blanked so it falls to `start_ap()`. Serial: `start=1 ... ch=1`, board stable.
- **`KSTouch-Config` now appears on the phone** after a WiFi rescan; onboarding proceeds. This
- was confirmed on hardware — the previous behaviour (invisible) is gone.
+ was confirmed on hardware — the previous behavior (invisible) is gone.
## Notes / follow-ups
diff --git a/tasks/LNK-001.md b/tasks/LNK-001.md
index 06022b4..520ed5f 100644
--- a/tasks/LNK-001.md
+++ b/tasks/LNK-001.md
@@ -13,7 +13,7 @@ updated_at: 2026-06-26
## Goal
Create the `esp32/X32Link/` Arduino sketch directory with a minimal WiFi
-sketch and a `config.h` that centralises all runtime parameters. Verify the
+sketch and a `config.h` that centralizes all runtime parameters. Verify the
bare sketch compiles and connects to WiFi before any Link or OSC code is added.
This task targets any ESP32-S3 board (XIAO ESP32-S3, ESP32-S3 Super Mini, etc.).
@@ -71,7 +71,7 @@ arduino-cli upload \
**Serial quirk**: On ESP32-S3 with USB CDC, open the serial port with
DTR=True, RTS=False BEFORE pressing RST. Hardware reset required after upload
-(same behaviour as ESP-001 / XIAO ESP32-S3).
+(same behavior as ESP-001 / XIAO ESP32-S3).
**config.h constants** — all values as `#define`:
diff --git a/tasks/LNK-003.md b/tasks/LNK-003.md
index ea0b04d..7e0a80d 100644
--- a/tasks/LNK-003.md
+++ b/tasks/LNK-003.md
@@ -1,6 +1,6 @@
---
id: LNK-003
-title: WiFi connection and Link session initialisation
+title: WiFi connection and Link session initialization
status: done
assignee: null
priority: high
diff --git a/tasks/LNK-005.md b/tasks/LNK-005.md
index 3695b6d..5833753 100644
--- a/tasks/LNK-005.md
+++ b/tasks/LNK-005.md
@@ -12,7 +12,7 @@ updated_at: 2026-06-26
## Goal
-Open a UDP socket to the XR18 and write the current BPM as a normalised float
+Open a UDP socket to the XR18 and write the current BPM as a normalized float
to `/fx/{slot}/par/02` whenever `g_bpm_changed` is set. This is the final
output stage — Link tempo → XR18 delay time.
@@ -23,13 +23,13 @@ output stage — Link tempo → XR18 delay time.
- [ ] `send_bpm_osc()` builds a valid OSC `/fx/{slot}/par/02 ,f {val}` packet
- [ ] OSC packet byte layout verified against `src/X32lib/Xsprint.c` reference
- [ ] XR18 FX slot delay time visibly changes in X-Air Edit when DAW BPM changes
-- [ ] BPM range tested: 60, 120, 180 BPM → correct normalised values (see Notes)
+- [ ] BPM range tested: 60, 120, 180 BPM → correct normalized values (see Notes)
## Notes
**Firmware scope**: `esp32/X32Link/` only. Do NOT touch `esp32/X32_emulator/`.
-**BPM → normalised float** (inline — cannot share `src/x32tap_logic.c`):
+**BPM → normalized float** (inline — cannot share `src/x32tap_logic.c`):
```c
// Mirrors src/x32tap_logic.c: ms_to_normalized(), TAP_MAX_MS = 3000
static float bpm_to_normalized(float bpm) {
@@ -41,7 +41,7 @@ static float bpm_to_normalized(float bpm) {
```
**Expected values**:
-| BPM | delay_ms | normalised |
+| BPM | delay_ms | normalized |
|------|----------|------------|
| 60 | 1000 ms | 0.3333 |
| 120 | 500 ms | 0.1667 |
diff --git a/tasks/LNK-009.md b/tasks/LNK-009.md
index 6389502..79d9344 100644
--- a/tasks/LNK-009.md
+++ b/tasks/LNK-009.md
@@ -53,7 +53,7 @@ same knobs under different prefixes:
## Constraints
-- No behaviour change: same values reach the same call sites, just sourced
+- No behavior change: same values reach the same call sites, just sourced
per-input instead of via a fixed macro.
- `DEFAULT_WIFI_*` / `DEFAULT_MIXER_IP` / `DEFAULT_MODEL` / `DEFAULT_FX_SLOT`
already share names — leave them.
diff --git a/tasks/LNK-011.md b/tasks/LNK-011.md
index 9a55ccc..f9cea73 100644
--- a/tasks/LNK-011.md
+++ b/tasks/LNK-011.md
@@ -60,7 +60,7 @@ PublishDecision bpm_publisher_step(float bpm, bool active, uint32_t now_ms);
- Pure C, no Arduino deps — buildable and testable on host (follow the
existing `test/test_link_protocol.c` + weak-`millis` pattern).
-- Preserve current behaviour exactly: bpm=0 ⇒ no send (mixer holds last value);
+- Preserve current behavior exactly: bpm=0 ⇒ no send (mixer holds last value);
send only on change-past-threshold OR per-bar refresh; respect min interval.
## Acceptance Criteria
diff --git a/tasks/LNK-012.md b/tasks/LNK-012.md
index 3e18e22..5718e59 100644
--- a/tasks/LNK-012.md
+++ b/tasks/LNK-012.md
@@ -34,7 +34,7 @@ no input-source field yet.
`TempoSourceKind` enum from LNK-008. Default = Link.
2. **NVS** (`app_config_nvs.cpp`): load/save the new key `input_src`; default to
Link when absent (first boot / post factory-reset).
-3. **Web form** (`web_config.cpp`): add a labelled dropdown
+3. **Web form** (`web_config.cpp`): add a labeled dropdown
"Tempo source: Ableton Link / USB MIDI clock"; submit persists it like the
other fields, then reboots (existing save→restart flow).
4. **Boot** (LNK-010 `setup`): `tempo_source_select(cfg.input_source)` before
diff --git a/tasks/LNK-031.md b/tasks/LNK-031.md
index 65ef607..d974552 100644
--- a/tasks/LNK-031.md
+++ b/tasks/LNK-031.md
@@ -68,7 +68,7 @@ in `link_measurement.c` stay as-is and are called by the session or the glue.
timeouts emit END_FAIL; different-peer and due-timer both retrigger; trigger
gated by `active`; pong carries prev-ghost / commits at READY.
- [x] `link_measurement_io.cpp` reduced to sockets + timing that translate the
- session's actions (`run_action`); behaviour faithfully ported (LNK-026
+ session's actions (`run_action`); behavior faithfully ported (LNK-026
fixes preserved as session actions).
- [x] Full host suite green (20 suites); firmware compiles 86%; `AGENTS.md` updated.
- [x] **Re-verified on hardware** (flashed; Link peer @ 127 BPM). Steady play:
diff --git a/tasks/LNK-039.md b/tasks/LNK-039.md
index 0ba36f9..135bd79 100644
--- a/tasks/LNK-039.md
+++ b/tasks/LNK-039.md
@@ -1,6 +1,6 @@
---
id: LNK-039
-title: "Beat LED uses the configured dot colours (bar-1 accent + brightness scale)"
+title: "Beat LED uses the configured dot colors (bar-1 accent + brightness scale)"
status: done
assignee: null
priority: medium
@@ -14,21 +14,21 @@ updated_at: 2026-07-08
On RGB-LED boards (QT Py NeoPixel, `LED_RGB`) the beat LED is hardcoded green
(`rgbLedWrite(pin, 0, 40, 0)`). The web/touch UI already has BEAT + BAR1
-colour pickers (LNK-036, `dot_beat_color`/`dot_accent_color`, NVS-persisted,
+color pickers (LNK-036, `dot_beat_color`/`dot_accent_color`, NVS-persisted,
live-applied via /live) — but they only drive the touch-display dot and web
dot. The physical LED ignores them, and shows no bar-1 accent at all.
-KitchenSync's strip (P4-019) already does colour + accent from the web UI.
+KitchenSync's strip (P4-019) already does color + accent from the web UI.
## Solution
- Pure `led_flash_rgb()` in `led_phase.{h,c}` (host-tested): pick accent vs
- beat colour from bar phase (int part 0 = downbeat, same rule as the touch
+ beat color from bar phase (int part 0 = downbeat, same rule as the touch
dot), scale channels by brightness/255 (40 keeps the old NeoPixel level).
-- `led_set()` RGB path flashes a stored current colour; `led_task` sets it
+- `led_set()` RGB path flashes a stored current color; `led_task` sets it
per flash from `g_config` + `tempo_source_phase(quantum_beats)`. Fallback
- beat / WiFi-down blink use the beat colour (no bar info there).
-- Non-RGB boards unchanged (plain GPIO ignores colour).
-- Web UI hint updated: colours drive dot + beat LED.
+ beat / WiFi-down blink use the beat color (no bar info there).
+- Non-RGB boards unchanged (plain GPIO ignores color).
+- Web UI hint updated: colors drive dot + beat LED.
## Deliverable
diff --git a/tasks/LNK-042.md b/tasks/LNK-042.md
index b297745..7531d79 100644
--- a/tasks/LNK-042.md
+++ b/tasks/LNK-042.md
@@ -61,7 +61,7 @@ inert for the other builds. Pick the scheme per board:
## Acceptance Criteria
- [ ] `X32Link/partitions.csv` exists: 16 MB, dual-OTA, 4 MB app slots, NVS + coredump,
- modelled on `KitchenSyncTouch/partitions.csv`.
+ modeled on `KitchenSyncTouch/partitions.csv`.
- [ ] The Waveshare X32Link build uses `PartitionScheme=custom`; the 4 MB builds keep
`min_spiffs` (LNK-041) and are unaffected by the new file.
- [ ] CI compiles the Waveshare variant against the 16 MB table and reports usage
diff --git a/tasks/MCK-005.md b/tasks/MCK-005.md
index 735afcc..91c6ec3 100644
--- a/tasks/MCK-005.md
+++ b/tasks/MCK-005.md
@@ -56,7 +56,7 @@ buffer just stays empty when disconnected; `midi_bpm_update()` returns 0.0 after
the timeout, and OSC stops. On reconnect, Ableton re-sends clock and the
firmware picks it back up within one beat.
-**Clock-stop behaviour (design decision)**: When the musician stops the DAW,
+**Clock-stop behavior (design decision)**: When the musician stops the DAW,
we intentionally do NOT reset the X32 tempo to zero or a default. The desk
keeps the last known tempo — the band can keep playing in time if the DAW
stops unexpectedly. Tempo only changes when a new valid BPM arrives.
diff --git a/tasks/MCK-006.md b/tasks/MCK-006.md
index 81dd3fc..0994288 100644
--- a/tasks/MCK-006.md
+++ b/tasks/MCK-006.md
@@ -27,7 +27,7 @@ DAW start/stop.
- [ ] Enabling "Clock" output in Ableton and pressing Play causes FX delay time
on XR18 to update within 1–2 beats
- [ ] BPM accuracy: set Ableton to 80, 120, 140, 180 BPM → X-Air Edit delay time
- matches expected normalised values (see LNK-005 reference table)
+ matches expected normalized values (see LNK-005 reference table)
- [ ] Tempo change: drag Ableton BPM from 120 → 140 → X32 follows within 1 beat
- [ ] DAW stop: FX delay time stays at last known value (does not reset)
- [ ] DAW start after stop: tempo resumes correctly within 1 beat
diff --git a/tasks/P4-004.md b/tasks/P4-004.md
index d5dd928..be2659c 100644
--- a/tasks/P4-004.md
+++ b/tasks/P4-004.md
@@ -22,7 +22,7 @@ depend on a live tempo.
The P4 SoC has no native WiFi radio — it's provided by the onboard ESP32-C6 over
the hosted-WiFi stack. Everything downstream needs a tempo, and Link-over-WiFi is
-the primary source (matching the shipped S3 behaviour).
+the primary source (matching the shipped S3 behavior).
## Open questions (the real work is here)
diff --git a/tasks/P4-016.md b/tasks/P4-016.md
index 346851e..e87d469 100644
--- a/tasks/P4-016.md
+++ b/tasks/P4-016.md
@@ -14,7 +14,7 @@ updated_at: 2026-07-11
Stand up a **headless, always-on ground-truth Ableton Link peer** on the Arduino
Uno Q's Linux side, running the **official Ableton Link SDK**, to verify the
-ESP32 devices' Link behaviour against a real peer — not against our own
+ESP32 devices' Link behavior against a real peer — not against our own
clean-room implementation.
## Why this exists
@@ -64,7 +64,7 @@ lock) against ground truth instead of self-comparison.
- Pure bench tooling — no change to the ESP32 firmware or the shared pure modules.
- Complements, doesn't replace, the host-tested pure logic; it covers exactly the
- on-the-wire behaviour that host tests can't.
+ on-the-wire behavior that host tests can't.
## Triage — 2026-07-11
diff --git a/tasks/P4-018.md b/tasks/P4-018.md
index c0c6b77..436e563 100644
--- a/tasks/P4-018.md
+++ b/tasks/P4-018.md
@@ -45,7 +45,7 @@ each beat so it pulses. You see where you are in the bar and the downbeat coming
## Status
-Built, host-tested, and flashed. Colours kept below full-scale (~0.5 A max at 8
+Built, host-tested, and flashed. Colors kept below full-scale (~0.5 A max at 8
px) for the eyes and the 5 V rail. Needs an on-hardware eyeball: strip chases in
time, amber downbeat, toggles live. 3.3 V data usually drives a short strip fine;
add a level shifter if the first pixels flicker.
diff --git a/tasks/P4-021.md b/tasks/P4-021.md
index 1a46709..4442ccc 100644
--- a/tasks/P4-021.md
+++ b/tasks/P4-021.md
@@ -14,7 +14,7 @@ updated_at: 2026-07-11
Emit **analog sync pulses** (0/+5V clock/trigger) on several outputs, each with its
own rate — to sync modular/Eurorack, vintage drum machines (DIN-sync / Sync24),
-and pedals, alongside the USB/MIDI clock. Realises the long-planned LNK-028 on the
+and pedals, alongside the USB/MIDI clock. Realizes the long-planned LNK-028 on the
P4 tier.
## Inspiration
diff --git a/tasks/P4-024.md b/tasks/P4-024.md
index fde1404..438e8ec 100644
--- a/tasks/P4-024.md
+++ b/tasks/P4-024.md
@@ -12,7 +12,7 @@ updated_at: 2026-07-11
## Goal
-Save and recall whole-device configurations (tempo behaviour, outputs, metronome,
+Save and recall whole-device configurations (tempo behavior, outputs, metronome,
LED, meter, ...) so you can switch between songs/setups without re-entering
settings.
diff --git a/tasks/P4-026.md b/tasks/P4-026.md
index 6a194fb..9f39650 100644
--- a/tasks/P4-026.md
+++ b/tasks/P4-026.md
@@ -21,7 +21,7 @@ that drives Link + all outputs. The tightest possible DAW sync.
JMK CLOCKstep:MULTI "Sample Accurate Clocking" — a DAW plugin sends sample-accurate
clock through the audio interface into the box's audio input to resolve USB-MIDI
jitter/latency (https://jmkmusicpedals.com/products/clockstep-multi). Their users
-call the audio-in sync a favourite feature.
+call the audio-in sync a favorite feature.
## Hardware gate
diff --git a/tasks/P4-029.md b/tasks/P4-029.md
index c88fb76..4b81f17 100644
--- a/tasks/P4-029.md
+++ b/tasks/P4-029.md
@@ -13,7 +13,7 @@ updated_at: 2026-07-07
## Problem
The P4 metronome VOL and VOICE required a Save + reboot to take effect, while the
-LED colours and the accent toggle already applied live via `/live`. There are no
+LED colors and the accent toggle already applied live via `/live`. There are no
audio files — the voice is a synthesized sine tone burst (`render_burst`, pitch +
length from `metronome_voice_params`) and volume is an ES8311 codec register — so
nothing needs "pulling"; they were just consumed once at boot in
diff --git a/tasks/P4-030.md b/tasks/P4-030.md
index 1add387..2ebb6cd 100644
--- a/tasks/P4-030.md
+++ b/tasks/P4-030.md
@@ -28,7 +28,7 @@ Follow the project's pure-engine / thin-glue split (ADR-0003):
- **Pure `rtp_midi_session`** (host-tested, like `link_measurement_session` /
`session_timeline`): owns the AppleMIDI protocol — the invitation handshake
- (IN/OK/NO on the control port), clock synchronisation (CK0/1/2 timestamps), and
+ (IN/OK/NO on the control port), clock synchronization (CK0/1/2 timestamps), and
RTP-MIDI packet framing (RTP header + MIDI command list; the recovery journal can be
a later refinement — start journal-less, most receivers accept it). No sockets here;
emit/consume actions like the other session state machines.
diff --git a/tasks/P4-036.md b/tasks/P4-036.md
index d35d59d..309098b 100644
--- a/tasks/P4-036.md
+++ b/tasks/P4-036.md
@@ -32,7 +32,7 @@ Two real obstacles, neither of them proven fatal:
decision.
2. **The P4 is an ESP-IDF build, not Arduino.** The flasher script drives `arduino-cli compile`
and harvests the Arduino export directory. The P4 needs `idf.py build` and a different set of
- artefacts. The CI job would need an IDF toolchain alongside `arduino-cli`.
+ artifacts. The CI job would need an IDF toolchain alongside `arduino-cli`.
## Why it matters more than it looks
diff --git a/tasks/P4-037.md b/tasks/P4-037.md
index 95b7487..e182940 100644
--- a/tasks/P4-037.md
+++ b/tasks/P4-037.md
@@ -61,7 +61,7 @@ so with a budget of 10 the existing 7 was already the ceiling. The pool was neve
## The actual fix
`CONFIG_LWIP_MAX_SOCKETS=16` in `KitchenSync/sdkconfig.defaults` — give the box enough sockets
-for its real tenants. `lru_purge_enable = true` is kept alongside it as defence in depth: it
+for its real tenants. `lru_purge_enable = true` is kept alongside it as defense in depth: it
is what stops httpd's own pool from refusing a client once the pool (not LWIP) is the thing
that fills.
diff --git a/tasks/P4-038.md b/tasks/P4-038.md
index 2768afa..caec3e9 100644
--- a/tasks/P4-038.md
+++ b/tasks/P4-038.md
@@ -42,7 +42,7 @@ reproducible on demand, and the trigger was never isolated.**
**The clock engine itself is sound.** At idle every bar is *exactly* 96 clocks (29/29 bars,
nothing dropped or doubled), mean 20.834 ms = 119.997 BPM, phase-locked with bounded error.
-The ~0.5 ms sd at idle is NOT jitter — it is the 1 kHz tick quantising a 20.833 ms period into
+The ~0.5 ms sd at idle is NOT jitter — it is the 1 kHz tick quantizing a 20.833 ms period into
a 20/21 ms mix (histogram: 20 ms x535, 21 ms x2281, averaging to 20.833). That is
`CONFIG_FREERTOS_HZ=1000` behaving exactly as P4-006 designed. Do not "fix" it.
diff --git a/test/test_app_config.c b/test/test_app_config.c
index 93f08f6..22eedf0 100644
--- a/test/test_app_config.c
+++ b/test/test_app_config.c
@@ -201,7 +201,7 @@ void test_set_model_ignores_invalid_model(void) {
TEST_ASSERT_EQUAL_INT(7, cfg.fx_slot);
}
-// LNK-036: phase display mode + dot colours.
+// LNK-036: phase display mode + dot colors.
void test_defaults_phase_display_is_sweep(void) {
AppConfig cfg; config_defaults(&cfg);
TEST_ASSERT_EQUAL_INT(0, cfg.phase_display_mode); // 0 = sweep wheel
diff --git a/test/test_beat_synth.c b/test/test_beat_synth.c
index b011033..1461970 100644
--- a/test/test_beat_synth.c
+++ b/test/test_beat_synth.c
@@ -7,7 +7,7 @@ void setUp(void) { beat_synth_reset(&s); }
void tearDown(void) {}
void test_fresh_state_fires_on_first_step_past_interval(void) {
- // Matches the old inline behaviour: static last=0, a large millis() reading
+ // Matches the old inline behavior: static last=0, a large millis() reading
// is >= one interval so the first observed beat fires.
TEST_ASSERT_TRUE(beat_synth_step(&s, 1000, 120.0f)); // interval 500, 1000-0 >= 500
}
diff --git a/test/test_clock_ticker.c b/test/test_clock_ticker.c
index 908a4de..915c9af 100644
--- a/test/test_clock_ticker.c
+++ b/test/test_clock_ticker.c
@@ -125,7 +125,7 @@ void test_normal_catchup_drops_nothing(void) {
// trusted (this test caught exactly that -- it read 25 instead of 24). Keeping a
// lifetime total across resets is the glue's job; a pure struct cannot tell "first
// init" from "re-prime".
-void test_reset_zeroes_dropped_so_it_is_always_initialised(void) {
+void test_reset_zeroes_dropped_so_it_is_always_initialized(void) {
ClockTicker t; clock_ticker_reset(&t);
clock_ticker_ticks_due(&t, 0.0, 24, 4);
clock_ticker_ticks_due(&t, 1.0, 24, 4); // 24 discarded
@@ -151,6 +151,6 @@ int main(void) {
RUN_TEST(test_bar_reset_bad_quantum);
RUN_TEST(test_realign_counts_the_pulses_it_discards);
RUN_TEST(test_normal_catchup_drops_nothing);
- RUN_TEST(test_reset_zeroes_dropped_so_it_is_always_initialised);
+ RUN_TEST(test_reset_zeroes_dropped_so_it_is_always_initialized);
return UNITY_END();
}
diff --git a/test/test_config_persist.c b/test/test_config_persist.c
index f488352..1781cb4 100644
--- a/test/test_config_persist.c
+++ b/test/test_config_persist.c
@@ -1,7 +1,7 @@
// Host tests for the ARC-022 debounced config write-through policy.
//
// The bug this exists to kill: /live applied a setting to the running config and
-// never wrote NVS, so every live-safe field -- metronome voice, LED colours, and
+// never wrote NVS, so every live-safe field -- metronome voice, LED colors, and
// the whole clock[] array including follow_link -- silently reverted on reboot.
// Writing on every /live POST is not the fix: config is ONE nvs blob, and a
// slider drag emits dozens of POSTs, so that would blob-write flash tens of times
@@ -10,7 +10,7 @@
// So a live edit marks the config dirty and the blob is written once the edits
// settle. That debounce is the only part of the fix with real logic in it, which
// is why it is the part that is pure and tested. Time is an INPUT: the test owns
-// the clock, so the timing behaviour is provable with no device, no flash, and no
+// the clock, so the timing behavior is provable with no device, no flash, and no
// power cycle.
#include "unity.h"
#include "config_persist.h"
diff --git a/test/test_ks_config.c b/test/test_ks_config.c
index 78fce51..5c7a895 100644
--- a/test/test_ks_config.c
+++ b/test/test_ks_config.c
@@ -189,7 +189,7 @@ void test_led_toggle(void) {
TEST_ASSERT_FALSE(ks_config_set(&c, "led", "2")); // only 0/1
}
-// Strip customization (P4-019): brightness, mode, fade, and #rrggbb colours.
+// Strip customization (P4-019): brightness, mode, fade, and #rrggbb colors.
void test_led_customization(void) {
TEST_ASSERT_TRUE(ks_config_set(&c, "led_bright", "40"));
TEST_ASSERT_EQUAL_INT(40, c.led_brightness);
@@ -272,7 +272,7 @@ void test_live_safe_copy_carries_live_fields_not_wifi(void) {
// ks_config_decode owns the "is this persisted blob safe to load?" decision, so
// a struct-layout change falls back to clean defaults instead of loading bytes
// shifted into the wrong fields. Pure: the caller (ks_config_nvs.c) hands over
-// whatever NVS held; every judgement happens here.
+// whatever NVS held; every judgment happens here.
// A blob that a CURRENT-version firmware would have written: right version,
// right size, valid contents.
@@ -465,7 +465,7 @@ void test_decode_unversioned_v1_sized_blob_still_defaults(void) {
}
// ESP-011 per-output transport master. Default 1 = Link owns transport once the
-// session publishes it -- the behaviour before this flag existed, so an upgraded
+// session publishes it -- the behavior before this flag existed, so an upgraded
// unit acts exactly as it used to until the user opts an output into manual.
void test_follow_link_defaults_to_link(void) {
KsConfig c; ks_config_defaults(&c);
diff --git a/test/test_ks_status.c b/test/test_ks_status.c
index 6775353..55bc0db 100644
--- a/test/test_ks_status.c
+++ b/test/test_ks_status.c
@@ -75,7 +75,7 @@ void test_launch_state_array_present(void) {
}
// Transport state: the session's real playing flag and whether Link owns
-// transport, so the UI can reflect play/stop and grey the manual buttons.
+// transport, so the UI can reflect play/stop and gray the manual buttons.
void test_transport_state_fields(void) {
char b[300];
ks_status_json(b, sizeof(b), 126.0f, 0.0f, 2, false, 0, "2.1.0", false, 0.0f, 0.0f, false, kNoLaunch, 4, true, true, NULL, NULL, NULL, NULL, NULL);
diff --git a/test/test_led_phase.c b/test/test_led_phase.c
index 573bcf5..28020f6 100644
--- a/test/test_led_phase.c
+++ b/test/test_led_phase.c
@@ -44,16 +44,16 @@ void test_prev_phase_zero_is_a_real_reading(void) {
// ---- led_flash_rgb (LNK-039) ----
-// Bar phase int part 0 (first beat of the bar) -> accent colour; any later
-// beat -> beat colour. Same rule as the touch dot's `(int)ts.phase == 0`.
+// Bar phase int part 0 (first beat of the bar) -> accent color; any later
+// beat -> beat color. Same rule as the touch dot's `(int)ts.phase == 0`.
void test_flash_rgb_accent_on_bar_downbeat(void) {
TEST_ASSERT_EQUAL_HEX32(0xFF0000, led_flash_rgb(0x00FF00, 0xFF0000, 0.25f, true, 255));
TEST_ASSERT_EQUAL_HEX32(0x00FF00, led_flash_rgb(0x00FF00, 0xFF0000, 1.75f, true, 255));
TEST_ASSERT_EQUAL_HEX32(0x00FF00, led_flash_rgb(0x00FF00, 0xFF0000, 3.00f, true, 255));
}
-// No bar phase available (sync gap / fallback beat) -> beat colour, never accent.
-void test_flash_rgb_invalid_bar_uses_beat_colour(void) {
+// No bar phase available (sync gap / fallback beat) -> beat color, never accent.
+void test_flash_rgb_invalid_bar_uses_beat_color(void) {
TEST_ASSERT_EQUAL_HEX32(0x00FF00, led_flash_rgb(0x00FF00, 0xFF0000, 0.0f, false, 255));
}
@@ -65,12 +65,12 @@ void test_flash_rgb_negative_phase_is_not_downbeat(void) {
}
// Brightness scales each channel by bright/255; 40 reproduces the old
-// hardcoded rgbLedWrite(pin, 0, 40, 0) level for a pure-green colour.
+// hardcoded rgbLedWrite(pin, 0, 40, 0) level for a pure-green color.
void test_flash_rgb_brightness_scales_channels(void) {
TEST_ASSERT_EQUAL_HEX32(0x002800, led_flash_rgb(0x00FF00, 0xFF0000, 1.5f, true, 40)); // 0x28 = 40
TEST_ASSERT_EQUAL_HEX32(0x280000, led_flash_rgb(0x00FF00, 0xFF0000, 0.5f, true, 40));
TEST_ASSERT_EQUAL_HEX32(0x000000, led_flash_rgb(0xFFFFFF, 0xFFFFFF, 1.5f, true, 0));
- // per-channel: mixed colour scales independently, floors toward zero
+ // per-channel: mixed color scales independently, floors toward zero
TEST_ASSERT_EQUAL_HEX32((0xB6 * 40 / 255) << 16 | (0xFF * 40 / 255) << 8 | (0x36 * 40 / 255),
led_flash_rgb(0xB6FF36, 0xFF9D3B, 1.5f, true, 40));
}
@@ -84,7 +84,7 @@ int main(void) {
RUN_TEST(test_equal_phase_does_not_flash);
RUN_TEST(test_prev_phase_zero_is_a_real_reading);
RUN_TEST(test_flash_rgb_accent_on_bar_downbeat);
- RUN_TEST(test_flash_rgb_invalid_bar_uses_beat_colour);
+ RUN_TEST(test_flash_rgb_invalid_bar_uses_beat_color);
RUN_TEST(test_flash_rgb_negative_phase_is_not_downbeat);
RUN_TEST(test_flash_rgb_brightness_scales_channels);
return UNITY_END();
diff --git a/test/test_link_measurement_session.c b/test/test_link_measurement_session.c
index 2ca72f4..40273be 100644
--- a/test/test_link_measurement_session.c
+++ b/test/test_link_measurement_session.c
@@ -24,7 +24,7 @@ static int start(uint32_t ip, uint16_t port, int64_t now) {
static void run_acts(int n) {
for (int i = 0; i < n; i++) {
switch (acts[i].type) {
- case LS_FLUSH_RX: break; // sockets: not modelled here
+ case LS_FLUSH_RX: break; // sockets: not modeled here
case LS_SEND_PING: break;
case LS_START_ATTEMPT: link_measurement_attempt_begin(); break;
case LS_END_OK: link_measurement_attempt_end(true); break;
diff --git a/test/test_metro_strip.c b/test/test_metro_strip.c
index a5e4898..b484a08 100644
--- a/test/test_metro_strip.c
+++ b/test/test_metro_strip.c
@@ -1,5 +1,5 @@
// Host tests for the pure customizable visual-metronome renderer (P4-018/019):
-// three patterns (chase / flash / fill), configurable colours, brightness, fade.
+// three patterns (chase / flash / fill), configurable colors, brightness, fade.
#include "unity.h"
#include "metro_strip.h"
#include
@@ -30,7 +30,7 @@ void test_chase_downbeat_first_block_accent(void) {
void test_chase_beat_two_block_beat_color(void) {
metro_strip_render(1.0, 4, METRO_STRIP_PIXELS, &cfg, px);
TEST_ASSERT_TRUE(px[2].g > 0 && px[3].g > 0);
- TEST_ASSERT_EQUAL_UINT8(0, px[2].r); // beat colour is green (no red)
+ TEST_ASSERT_EQUAL_UINT8(0, px[2].r); // beat color is green (no red)
TEST_ASSERT_EQUAL_INT(2, count());
}
@@ -61,7 +61,7 @@ void test_flash_lights_all_pixels(void) {
cfg.mode = METRO_STRIP_FLASH;
metro_strip_render(1.0, 4, METRO_STRIP_PIXELS, &cfg, px);
TEST_ASSERT_EQUAL_INT(METRO_STRIP_PIXELS, count());
- TEST_ASSERT_EQUAL_UINT8(0, px[5].r); // beat colour (green)
+ TEST_ASSERT_EQUAL_UINT8(0, px[5].r); // beat color (green)
}
void test_flash_downbeat_is_accent(void) {
@@ -91,7 +91,7 @@ void test_fill_grows(void) {
TEST_ASSERT_TRUE(count() > a);
}
-// --- BRIGHTNESS + COLOURS ------------------------------------------------
+// --- BRIGHTNESS + COLORS ------------------------------------------------
void test_brightness_scales_down(void) {
metro_strip_render(2.0, 4, METRO_STRIP_PIXELS, &cfg, px); // bright 100
@@ -111,7 +111,7 @@ void test_custom_colors_used(void) {
/* ---- ESP-009: standby heartbeat ---------------------------------------- */
// Separate entry point from metro_strip_render: standby is not a beat pattern,
// it has no beat. Slow sinusoidal breath over the whole strip in the beat
-// colour, dim, so "waiting for play" reads as alive-and-idle.
+// color, dim, so "waiting for play" reads as alive-and-idle.
// Trough and peak of the breath differ, and neither is fully dark -- a strip
// that reaches zero looks like a board that crashed between blinks.
diff --git a/test/test_status_rgb.c b/test/test_status_rgb.c
index 2688701..8ede244 100644
--- a/test/test_status_rgb.c
+++ b/test/test_status_rgb.c
@@ -1,4 +1,4 @@
-// ESP-043: the Super Mini's onboard status RGB. The state->colour mapping is a pure
+// ESP-043: the Super Mini's onboard status RGB. The state->color mapping is a pure
// function so it can be asserted here rather than eyeballed on the glass -- a screenless
// box's only feedback has to be right without a person watching it.
#include "unity.h"
@@ -30,14 +30,14 @@ void test_running_flashes_on_the_beat_and_is_dark_between(void) {
// Free-run is green, Link-locked is cyan -- you can tell across the room whether it caught
// the session. Green has no blue; cyan has blue.
-void test_running_colour_tells_free_from_link(void) {
+void test_running_color_tells_free_from_link(void) {
StatusRgb freerun = ktouch_status_rgb(TL_RUNNING, false, ON_BEAT2, 4);
StatusRgb locked = ktouch_status_rgb(TL_RUNNING, true, ON_BEAT2, 4);
TEST_ASSERT_TRUE(freerun.g > 0 && freerun.b == 0); // green
TEST_ASSERT_TRUE(locked.b > 0 && locked.g > 0); // cyan (green+blue)
}
-// The bar-1 downbeat is a brighter accent of the same colour, so the "one" reads without a
+// The bar-1 downbeat is a brighter accent of the same color, so the "one" reads without a
// hue change that would fight the free/Link signal.
void test_downbeat_is_brighter_than_the_other_beats(void) {
StatusRgb down = ktouch_status_rgb(TL_RUNNING, false, ON_BEAT, 4); // whole 4 => beat 1
@@ -58,7 +58,7 @@ int main(void) {
UNITY_BEGIN();
RUN_TEST(test_stopped_is_dark_even_on_the_beat);
RUN_TEST(test_running_flashes_on_the_beat_and_is_dark_between);
- RUN_TEST(test_running_colour_tells_free_from_link);
+ RUN_TEST(test_running_color_tells_free_from_link);
RUN_TEST(test_downbeat_is_brighter_than_the_other_beats);
RUN_TEST(test_armed_blinks_amber_on_the_beat);
return UNITY_END();
diff --git a/test/test_wifi_conn_policy.c b/test/test_wifi_conn_policy.c
index 06df40f..dba8148 100644
--- a/test/test_wifi_conn_policy.c
+++ b/test/test_wifi_conn_policy.c
@@ -4,7 +4,7 @@
#include "wifi_conn_policy.h"
static WifiConnPolicy p;
-void setUp(void) { wifi_conn_policy_reset(&p, 0, 1); } // one slot = pre-ESP-013 behaviour
+void setUp(void) { wifi_conn_policy_reset(&p, 0, 1); } // one slot = pre-ESP-013 behavior
void tearDown(void) {}
/* ---- cold-start budget (was wifi_fallback) ----------------------------- */
diff --git a/tools/build-flasher-site.sh b/tools/build-flasher-site.sh
index 52bbe07..91d1c63 100755
--- a/tools/build-flasher-site.sh
+++ b/tools/build-flasher-site.sh
@@ -22,7 +22,7 @@ CACHE_ARG=()
rm -rf "$SITE"
mkdir -p "$SITE/firmware"
-# boot_app0.bin initialises otadata to boot app0. The core writes it on a normal upload,
+# boot_app0.bin initializes otadata to boot app0. The core writes it on a normal upload,
# so we write it too. A blank otadata would ALSO boot app0 (the bootloader falls back),
# but relying on fallback is not the same as stating intent -- and this is the file that
# decides which slot a freshly-flashed device runs from.
diff --git a/tools/linkcli/README.md b/tools/linkcli/README.md
index 668f686..c6b9e88 100644
--- a/tools/linkcli/README.md
+++ b/tools/linkcli/README.md
@@ -41,7 +41,7 @@ with asio bundled under `modules/asio-standalone`. Override the SDK path with
**A peer that joins a session that is already playing 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`
+a booting peer initializes its start/stop state with `timestamp = hostTime`
(`Controller.hpp:67`) — so its fresh "stopped" is *newer* than your earlier "playing" and
wins. Tempo, by contrast, *is* adopted by late joiners, which makes the asymmetry extra
confusing.