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
30 changes: 30 additions & 0 deletions KitchenSync/main/transport_button.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ESP-045: single-button transport gesture — see transport_button.h. Pure.
#include "transport_button.h"

void transport_button_reset(TransportButton* tb) {
button_reset(&tb->btn);
tb->down = false;
tb->press_ms = 0;
tb->hold_fired = false;
}

ButtonGesture transport_button_update(TransportButton* tb, bool raw_pressed, uint32_t now_ms) {
if (button_update(&tb->btn, raw_pressed, now_ms)) { // debounced press edge
tb->down = true;
tb->press_ms = now_ms;
tb->hold_fired = false;
return BTN_GESTURE_NONE;
}
if (tb->down && !button_is_pressed(&tb->btn)) { // released
tb->down = false;
return tb->hold_fired ? BTN_GESTURE_NONE : BTN_GESTURE_TAP;
}
// held past the threshold, still down: realign — fire once, and the later release
// stays silent (hold_fired short-circuits the TAP above).
if (tb->down && !tb->hold_fired && button_is_pressed(&tb->btn)
&& (now_ms - tb->press_ms) >= TRANSPORT_BUTTON_HOLD_MS) {
tb->hold_fired = true;
return BTN_GESTURE_HOLD;
}
return BTN_GESTURE_NONE;
}
42 changes: 42 additions & 0 deletions KitchenSync/main/transport_button.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once
// ESP-045: one physical button, two gestures — the Mini's live transport surface.
// A short press TAPS (play/stop toggle); holding past a threshold is a HOLD (realign).
// Pure timing + debounce over buttons.h, so the whole gesture split is host-testable;
// the glue owns only the pin read and mapping the gesture to a TransportLaunchIntent
// (TAP -> play/stop per launch state, HOLD -> realign). No intent/launch-state
// dependency here on purpose — this module decides ONLY short-vs-long.
#include <stdbool.h>
#include <stdint.h>
#include "buttons.h"

#ifdef __cplusplus
extern "C" {
#endif

// Held this long (from the debounced press edge) turns a press into a HOLD. Short
// enough to feel deliberate, long enough that a normal tap never trips it.
#define TRANSPORT_BUTTON_HOLD_MS 600

typedef enum {
BTN_GESTURE_NONE = 0,
BTN_GESTURE_TAP, // short press, reported on release -> play/stop toggle
BTN_GESTURE_HOLD, // held past the threshold, reported once while still down -> realign
} ButtonGesture;

typedef struct {
Button btn; // debounce (edge + level)
bool down; // a debounced press is in progress, not yet resolved
uint32_t press_ms; // when the debounced press edge landed
bool hold_fired; // HOLD already emitted for this press (so release is silent)
} TransportButton;

void transport_button_reset(TransportButton* tb);

// Feed the raw pin level (true = pressed) and the current time. Returns a gesture at
// most once per press: HOLD the moment the press crosses TRANSPORT_BUTTON_HOLD_MS while
// still down; otherwise TAP when the press is released. NONE on every other tick.
ButtonGesture transport_button_update(TransportButton* tb, bool raw_pressed, uint32_t now_ms);

#ifdef __cplusplus
}
#endif
9 changes: 7 additions & 2 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ TOUCHUI = ../X32Link/touch_ui.c
SNAP = ../X32Link/tempo_snapshot.c
BAR = ../X32Link/bar.c
BUTTONS = ../X32Link/buttons.c
TBTN = ../KitchenSync/main/transport_button.c
TLED = ../X32Link/transport_led.c
TLAUNCH = ../X32Link/transport_launch.c
MIDIOUT = ../X32Link/midi_clock_out.c
Expand Down Expand Up @@ -74,7 +75,7 @@ HEADERS = $(wildcard ../X32Link/*.h ../KitchenSync/main/*.h ../LoraLink/*.h \

.PHONY: all clean

all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_web_status_json test_touch_ui test_tempo_snapshot test_bar test_midi_clock_out test_clock_ticker test_beat_synth test_link_measurement_session test_session_timeline test_link_measure_pump test_ks_tick test_follow_beat test_usb_midi_pack test_beat_clock test_ks_status test_ks_config test_ks_config_json test_ks_form test_metronome test_transport test_clock_output test_metronome_voice test_beat_source test_swing test_midi_clock_in test_metro_strip test_wifi_conn_policy test_lora_bpm_packet test_lora_freshness test_battery_gauge test_beat_stamper test_transport_launch test_ks_hostname test_ui_chrome test_ktouch_ui test_ktouch_migrate test_status_rgb test_usb_midi_batch test_config_persist test_master_clock test_buttons test_transport_led
all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_web_status_json test_touch_ui test_tempo_snapshot test_bar test_midi_clock_out test_clock_ticker test_beat_synth test_link_measurement_session test_session_timeline test_link_measure_pump test_ks_tick test_follow_beat test_usb_midi_pack test_beat_clock test_ks_status test_ks_config test_ks_config_json test_ks_form test_metronome test_transport test_clock_output test_metronome_voice test_beat_source test_swing test_midi_clock_in test_metro_strip test_wifi_conn_policy test_lora_bpm_packet test_lora_freshness test_battery_gauge test_beat_stamper test_transport_launch test_ks_hostname test_ui_chrome test_ktouch_ui test_ktouch_migrate test_status_rgb test_usb_midi_batch test_config_persist test_master_clock test_buttons test_transport_led test_transport_button
./test_bpm_tracker
./test_link_protocol
./test_link_measurement
Expand All @@ -91,6 +92,7 @@ all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase t
./test_tempo_snapshot
./test_bar
./test_buttons
./test_transport_button
./test_transport_led
./test_midi_clock_out
./test_clock_ticker
Expand Down Expand Up @@ -181,6 +183,9 @@ test_bar: test_bar.c $(BAR) $(UNITY) $(HEADERS)
test_buttons: test_buttons.c $(BUTTONS) $(UNITY) $(HEADERS)
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@

test_transport_button: test_transport_button.c $(TBTN) $(BUTTONS) $(UNITY) $(HEADERS)
$(CC) $(CFLAGS) -I../KitchenSync/main $(filter %.c,$^) -o $@

test_transport_led: test_transport_led.c $(TLED) $(TLAUNCH) $(UNITY) $(HEADERS)
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ -lm

Expand Down Expand Up @@ -233,7 +238,7 @@ test_battery_gauge: test_battery_gauge.c $(BATTGAUGE) $(UNITY) $(HEADERS)
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ -lm

clean:
rm -f test_buttons test_transport_led test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_web_status_json test_axs5106l test_touch_ui test_tempo_snapshot test_bar
rm -f test_transport_button test_buttons test_transport_led test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_web_status_json test_axs5106l test_touch_ui test_tempo_snapshot test_bar

test_config_persist: test_config_persist.c $(CFGPERSIST) $(UNITY) $(HEADERS)
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@
Expand Down
60 changes: 60 additions & 0 deletions test/test_transport_button.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "unity.h"
#include "transport_button.h"

void setUp(void) {}
void tearDown(void) {}

// A quick press-and-release (held well under the hold threshold) is a TAP — the
// play/stop toggle. Reported on RELEASE, so a press can still become a HOLD.
static void test_quick_press_release_is_a_tap(void) {
TransportButton tb; transport_button_reset(&tb);
transport_button_update(&tb, false, 0); // seed: released
transport_button_update(&tb, true, 100); // press (bouncing)
transport_button_update(&tb, true, 100 + BUTTON_DEBOUNCE_MS); // debounced press edge
transport_button_update(&tb, true, 200); // held briefly (< HOLD_MS)
transport_button_update(&tb, false, 300); // release (bouncing)
ButtonGesture g = transport_button_update(&tb, false, 300 + BUTTON_DEBOUNCE_MS); // settled
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_TAP, g);
}

// Holding past the threshold is a HOLD (realign) — fired ONCE the moment it crosses,
// while still down. The release afterward is silent, not a second TAP.
static void test_long_hold_fires_once_then_silent_release(void) {
TransportButton tb; transport_button_reset(&tb);
transport_button_update(&tb, false, 0); // seed: released
transport_button_update(&tb, true, 100); // press (bouncing)
transport_button_update(&tb, true, 100 + BUTTON_DEBOUNCE_MS); // debounced press edge (t=125)
uint32_t edge = 100 + BUTTON_DEBOUNCE_MS;
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_NONE, transport_button_update(&tb, true, edge + 300)); // < HOLD
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_HOLD, transport_button_update(&tb, true, edge + TRANSPORT_BUTTON_HOLD_MS));
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_NONE, transport_button_update(&tb, true, edge + TRANSPORT_BUTTON_HOLD_MS + 200)); // not again
transport_button_update(&tb, false, edge + 2000); // release (bouncing)
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_NONE, transport_button_update(&tb, false, edge + 2000 + BUTTON_DEBOUNCE_MS));
}

// Back-to-back gestures: a HOLD must not poison the next press. After a hold+release,
// a fresh short press is a clean TAP again (the press edge re-arms the state).
static void test_hold_then_next_press_is_a_clean_tap(void) {
TransportButton tb; transport_button_reset(&tb);
transport_button_update(&tb, false, 0);
transport_button_update(&tb, true, 100);
uint32_t e1 = 100 + BUTTON_DEBOUNCE_MS;
transport_button_update(&tb, true, e1); // press edge
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_HOLD, transport_button_update(&tb, true, e1 + TRANSPORT_BUTTON_HOLD_MS));
transport_button_update(&tb, false, e1 + 1000); // release (bouncing)
transport_button_update(&tb, false, e1 + 1000 + BUTTON_DEBOUNCE_MS); // settled (silent)
uint32_t t2 = e1 + 2000; // second press: a tap
transport_button_update(&tb, true, t2);
uint32_t e2 = t2 + BUTTON_DEBOUNCE_MS;
transport_button_update(&tb, true, e2); // press edge
transport_button_update(&tb, false, e2 + 100); // release (bouncing)
TEST_ASSERT_EQUAL_INT(BTN_GESTURE_TAP, transport_button_update(&tb, false, e2 + 100 + BUTTON_DEBOUNCE_MS));
}

int main(void) {
UNITY_BEGIN();
RUN_TEST(test_quick_press_release_is_a_tap);
RUN_TEST(test_long_hold_fires_once_then_silent_release);
RUN_TEST(test_hold_then_next_press_is_a_clean_tap);
return UNITY_END();
}
Loading