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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions KitchenSync/main/ks_tick.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,14 @@ KsTickPlan ks_tick_step(KsTickState* st, const KsTickInputs* in) {
st->tr[o].playing = false;
break;
case TL_RESTART:
// Unreachable today: the P4 posts no TL_INTENT_REALIGN (only the
// ESP-025 bench rig does). Supporting it here needs a plan-level
// change -- a restart is TWO bytes (0xFC then 0xFA) in one tick,
// and plan.transport[] holds exactly one action per output. Left
// as an explicit no-op rather than silently degrading to Stop.
// ESP-026: realign — stop THEN start, on the bar line. The plan
// carries it as one RESTART action; ks_tick_out_bytes expands it to
// 0xFC then 0xFA (both before the downbeat 0xF8 — the ESP-038
// transport-before-clock invariant). Still playing afterward, so a
// later session edge does not re-emit (mirror TL_START's tr sync).
plan.transport[o] = TRANSPORT_RESTART;
st->tr[o].primed = true;
st->tr[o].playing = true;
break;
case TL_NONE:
default:
Expand Down Expand Up @@ -201,6 +204,10 @@ int ks_tick_out_bytes(const KsTickPlan* p, int out, uint8_t* buf, int cap) {
* this lives in one tested place instead of in glue loop order. */
if (p->transport[out] == TRANSPORT_START && n < cap) buf[n++] = 0xFA;
else if (p->transport[out] == TRANSPORT_STOP && n < cap) buf[n++] = 0xFC;
else if (p->transport[out] == TRANSPORT_RESTART) { // realign: stop THEN start, one tick
if (n < cap) buf[n++] = 0xFC;
if (n < cap) buf[n++] = 0xFA;
}
for (int i = 0; i < p->pulses[out] && n < cap; i++) buf[n++] = 0xF8;
return n;
}
1 change: 1 addition & 0 deletions X32Link/transport.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ typedef enum {
TRANSPORT_NONE = 0,
TRANSPORT_START, // Link went stopped -> playing (emit 0xFA)
TRANSPORT_STOP, // Link went playing -> stopped (emit 0xFC)
TRANSPORT_RESTART, // realign on the bar line: stop THEN start (emit 0xFC then 0xFA)
} TransportAction;

typedef struct {
Expand Down
3 changes: 2 additions & 1 deletion tasks/ESP-026.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ Board differences go behind **Kconfig**, not `#define` comment-swaps — which a
repointed; all 56 host suites still pass
- [ ] Analyzer check on the S3 and the bench ESP32: **transport precedes clock** on the
downbeat tick (the ESP-023 regression cannot come back)
- [ ] `TL_RESTART` (realign) works on the P4, or is explicitly deferred with a reason
- [x] `TL_RESTART` (realign) works on the P4 — `TRANSPORT_RESTART` carries stop-then-start
through the plan; `ks_tick_out_bytes` emits `0xFC` then `0xFA` before the clock (host-tested)
- [ ] The AGENTS.md firmware map and the `X32Link.ino` banner updated to match the new layout

## Cross-references (2026-07-15)
Expand Down
44 changes: 44 additions & 0 deletions test/test_ks_tick.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ void test_start_byte_precedes_first_clock(void) {
TEST_ASSERT_LESS_THAN_INT(f8, fa); // START strictly before first CLOCK
}

// ESP-038 for realign: the restart pair (0xFC then 0xFA) must both precede the downbeat
// 0xF8 — same tick-late invariant as START, now with two transport bytes.
void test_restart_pair_precedes_first_clock(void) {
KsTickPlan p; memset(&p, 0, sizeof p);
p.transport[0] = TRANSPORT_RESTART;
p.pulses[0] = 3;
uint8_t buf[8];
int n = ks_tick_out_bytes(&p, 0, buf, sizeof buf);
int fc = idx_of(buf, n, 0xFC);
int fa = idx_of(buf, n, 0xFA);
int f8 = idx_of(buf, n, 0xF8);
TEST_ASSERT_EQUAL_INT(5, n); // FC + FA + 3 clocks
TEST_ASSERT_LESS_THAN_INT(f8, fc); // STOP before first CLOCK
TEST_ASSERT_LESS_THAN_INT(f8, fa); // START before first CLOCK
TEST_ASSERT_LESS_THAN_INT(fa, fc); // and STOP strictly before START
}

void test_none_emits_only_clocks(void) {
KsTickPlan p; memset(&p, 0, sizeof p);
p.transport[0] = TRANSPORT_NONE;
Expand All @@ -59,6 +76,18 @@ void test_stop_emits_fc(void) {
TEST_ASSERT_EQUAL_HEX8(0xFC, buf[0]);
}

// ESP-026: a realign (TL_RESTART) is a stop-then-start on the bar line — 0xFC THEN
// 0xFA, in that order, as one tick's transport edge. Was a no-op on the P4 writer.
void test_restart_emits_fc_then_fa(void) {
KsTickPlan p; memset(&p, 0, sizeof p);
p.transport[0] = TRANSPORT_RESTART;
uint8_t buf[8];
int n = ks_tick_out_bytes(&p, 0, buf, sizeof buf);
TEST_ASSERT_EQUAL_INT(2, n);
TEST_ASSERT_EQUAL_HEX8(0xFC, buf[0]);
TEST_ASSERT_EQUAL_HEX8(0xFA, buf[1]);
}

void test_empty_plan_emits_nothing(void) {
KsTickPlan p; memset(&p, 0, sizeof p); // NONE + 0 pulses
uint8_t buf[8];
Expand Down Expand Up @@ -304,6 +333,18 @@ void test_stop_is_immediate_per_output(void) {
TEST_ASSERT_EQUAL_INT(TRANSPORT_STOP, ks_tick_step(&st, &b).transport[0]);
}

// ESP-026: through the full tick, a realign while running + on the bar line drives
// TRANSPORT_RESTART on that output. Was a silent no-op — transport_launch produced
// TL_RESTART but ks_tick dropped it on the floor, so the writer emitted nothing.
void test_realign_drives_restart_through_the_tick(void) {
KsTickInputs a = mk(0, 0, true, true);
a.launch[0] = TL_INTENT_PLAY;
ks_tick_step(&st, &a); // output 0 running, on the grid
KsTickInputs b = mk((int64_t)(4 * MPB), 0, true, true); // beats = 4.0 — a bar line
b.launch[0] = TL_INTENT_REALIGN;
TEST_ASSERT_EQUAL_INT(TRANSPORT_RESTART, ks_tick_step(&st, &b).transport[0]);
}

// A disabled output never emits transport, whatever it is asked for.
void test_disabled_output_emits_no_transport(void) {
cfg.clock[0].enable = 0;
Expand Down Expand Up @@ -490,6 +531,7 @@ int main(void) {
RUN_TEST(test_manual_owns_transport_when_session_silent);
RUN_TEST(test_launch_tracks_without_usb);
RUN_TEST(test_transport_is_per_output);
RUN_TEST(test_realign_drives_restart_through_the_tick);
RUN_TEST(test_play_mid_bar_fires_on_the_downbeat);
RUN_TEST(test_stop_is_immediate_per_output);
RUN_TEST(test_disabled_output_emits_no_transport);
Expand All @@ -514,6 +556,8 @@ int main(void) {
RUN_TEST(test_start_byte_precedes_first_clock);
RUN_TEST(test_none_emits_only_clocks);
RUN_TEST(test_stop_emits_fc);
RUN_TEST(test_restart_emits_fc_then_fa);
RUN_TEST(test_restart_pair_precedes_first_clock);
RUN_TEST(test_empty_plan_emits_nothing);
RUN_TEST(test_cap_never_overflows);
return UNITY_END();
Expand Down
Loading