From 19a16187afb8b1dfdbf48a8390118aebd92838e6 Mon Sep 17 00:00:00 2001 From: banst Date: Mon, 26 Jan 2026 20:36:55 +0100 Subject: [PATCH] feat: add factory reset functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hold Setup button for 3 seconds to reset all settings to defaults: - 1 second: Medium red warning on grid - 2 seconds: Full red warning on grid - 3 seconds: Execute reset, green success pattern Features: - Resets all 4 pages to default settings - Saves defaults to flash - Reinitializes application state - Short press (< 1s) still toggles setup mode Implementation: - Add render_factory_reset_warning_slow/fast/success() in layout.c - Add flash_factory_reset() in flash.c - Track setup button hold time in app.c timer event - Use volatile for shared timer/interrupt variables - Fix FlashData alignment for ARM Cortex-M3 Closes #22 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- include/flash.h | 14 ++++--- include/layout.h | 18 +++++++++ src/app.c | 92 +++++++++++++++++++++++++++++++++++++++------- src/flash.c | 9 +++++ src/layout.c | 53 ++++++++++++++++++++++++++ tests/flash_test.c | 59 +++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 20 deletions(-) diff --git a/include/flash.h b/include/flash.h index bbb0fd7..6e3d06b 100644 --- a/include/flash.h +++ b/include/flash.h @@ -7,15 +7,17 @@ #define FLASH_VERSION 7 typedef struct FlashData { - unsigned long magic; - unsigned char version; - unsigned char current_page; - unsigned char reserved[2]; - Page pages[PAGE_COUNT]; -} __attribute__((packed)) FlashData; + unsigned long magic; // 4 bytes (offset 0) + unsigned char version; // 1 byte (offset 4) + unsigned char current_page; // 1 byte (offset 5) + unsigned char reserved[2]; // 2 bytes (offset 6) + // Total: 8 bytes, naturally 8-byte aligned for pages array + Page pages[PAGE_COUNT]; // offset 8, 8-byte aligned +} FlashData; void flash_load(void); void flash_save(void); unsigned char flash_is_valid(void); +void flash_factory_reset(void); #endif // FLASH_H diff --git a/include/layout.h b/include/layout.h index cebf0d4..158c45f 100644 --- a/include/layout.h +++ b/include/layout.h @@ -108,4 +108,22 @@ void process_pending_render(void); */ void invalidate_led_cache(void); +/** + * Render factory reset warning pattern (slow) + * Red medium brightness on grid for first warning phase + */ +void render_factory_reset_warning_slow(void); + +/** + * Render factory reset warning pattern (fast) + * Red full brightness on grid for second warning phase + */ +void render_factory_reset_warning_fast(void); + +/** + * Render factory reset success pattern + * Green on grid to indicate successful reset + */ +void render_factory_reset_success(void); + #endif // LAYOUT_H diff --git a/src/app.c b/src/app.c index c0006b4..c7130c3 100644 --- a/src/app.c +++ b/src/app.c @@ -40,7 +40,14 @@ #include "surface.h" #include "velocity.h" -static u16 g_timer_count = 0; +static volatile u16 g_timer_count = 0; + +// Factory reset state +// Note: These are accessed from both timer and surface events +// volatile ensures proper memory access semantics +static volatile u16 setup_hold_start = 0; +static volatile u8 factory_reset_phase = + 0; // 0=none, 1=warning1, 2=warning2, 3=success static unsigned char is_grid_pad(u8 index) { u8 row = index / 10; @@ -404,23 +411,42 @@ static void handle_setup_mode(u8 index, u8 value) { // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void app_surface_event(u8 type, u8 index, u8 value) { - // Setup button: toggle setup mode + // Setup button: toggle setup mode or factory reset if (type == TYPESETUP) { if (value) { - if (is_in_setup_mode()) { - // Exit setup mode - save if dirty - if (is_dirty()) { - flash_save(); - clear_dirty(); + // Button pressed - start tracking hold time + setup_hold_start = g_timer_count; + factory_reset_phase = 0; + } else { + // Button released + // Compute time difference with wrap-around handling + // Subtraction of u16 naturally handles wrap-around in modular arithmetic + u16 held_ms = g_timer_count - setup_hold_start; + + // Only toggle setup mode if held < 1s and not in warning/success phase + if (factory_reset_phase == 0 && held_ms < 1000) { + if (is_in_setup_mode()) { + // Exit setup mode - save if dirty + if (is_dirty()) { + flash_save(); + clear_dirty(); + } + exit_setup_mode(); + } else { + // Enter setup mode + clear_all_active_notes(); + clear_all_pad_notes(); + enter_setup_mode(); } - exit_setup_mode(); - } else { - // Enter setup mode - clear_all_active_notes(); - clear_all_pad_notes(); - enter_setup_mode(); + refresh_display(); + } else if (factory_reset_phase > 0 && factory_reset_phase < 3) { + // Button released during warning phase - clear warning display + refresh_display(); } - refresh_display(); + + // Reset factory reset state + setup_hold_start = 0; + factory_reset_phase = 0; } return; } @@ -528,6 +554,44 @@ void app_timer_event(void) { refresh_display(); } + // Factory reset hold timer + if (setup_hold_start != 0) { + // Compute time difference with wrap-around handling + // Subtraction of u16 naturally handles wrap-around in modular arithmetic + u16 held_ms = g_timer_count - setup_hold_start; + + if (held_ms >= 3000 && factory_reset_phase < 3) { + // 3 seconds - execute factory reset + flash_factory_reset(); + app_state_init(); + render_factory_reset_success(); + factory_reset_phase = 3; + setup_hold_start = 0; // Stop timing + } else if (held_ms >= 2000 && factory_reset_phase < 2) { + // 2 seconds - show fast warning + render_factory_reset_warning_fast(); + factory_reset_phase = 2; + } else if (held_ms >= 1000 && factory_reset_phase < 1) { + // 1 second - show slow warning + render_factory_reset_warning_slow(); + factory_reset_phase = 1; + } + } + + // Handle success pattern display + static u16 success_timer = 0; + if (factory_reset_phase == 3) { + success_timer++; + if (success_timer >= 500) { + // After 500ms, refresh display and reset + refresh_display(); + factory_reset_phase = 0; + success_timer = 0; + } + } else { + success_timer = 0; + } + // Process any pending render requests (deferred rendering optimization) process_pending_render(); diff --git a/src/flash.c b/src/flash.c index 5e25776..618c0d8 100644 --- a/src/flash.c +++ b/src/flash.c @@ -80,3 +80,12 @@ unsigned char flash_is_valid(void) { hal_read_flash(0, (u8 *)&data, sizeof(FlashData)); return data.magic == FLASH_MAGIC && data.version == FLASH_VERSION; } + +void flash_factory_reset(void) { + Page pages[PAGE_COUNT]; + for (unsigned char i = 0; i < PAGE_COUNT; i++) { + pages[i] = DEFAULT_PAGE; + } + page_manager_set(pages, 0); + flash_save(); +} diff --git a/src/layout.c b/src/layout.c index 01ac137..a7375cc 100644 --- a/src/layout.c +++ b/src/layout.c @@ -377,3 +377,56 @@ void invalidate_led_cache(void) { led_cache[i] = LED_CACHE_INVALID; } } + +void render_factory_reset_warning_slow(void) { + // Red medium brightness on all grid pads + u32 warning_color = (BRIGHT_MED << COLOR_RED_SHIFT); + for (u8 row = 1; row <= 8; row++) { + for (u8 col = 1; col <= 8; col++) { + u8 index = row * 10 + col; + set_pad_color(index, warning_color); + } + } + // Clear control buttons + for (u8 i = 0; i < GRID_SIZE; i++) { + if ((i / 10 >= 1 && i / 10 <= 8 && i % 10 >= 1 && i % 10 <= 8)) { + continue; // Skip grid pads + } + set_pad_color(i, COLOR_BLACK); + } +} + +void render_factory_reset_warning_fast(void) { + // Red full brightness on all grid pads + u32 warning_color = COLOR_RED; + for (u8 row = 1; row <= 8; row++) { + for (u8 col = 1; col <= 8; col++) { + u8 index = row * 10 + col; + set_pad_color(index, warning_color); + } + } + // Clear control buttons + for (u8 i = 0; i < GRID_SIZE; i++) { + if ((i / 10 >= 1 && i / 10 <= 8 && i % 10 >= 1 && i % 10 <= 8)) { + continue; // Skip grid pads + } + set_pad_color(i, COLOR_BLACK); + } +} + +void render_factory_reset_success(void) { + // Green full brightness on all grid pads + for (u8 row = 1; row <= 8; row++) { + for (u8 col = 1; col <= 8; col++) { + u8 index = row * 10 + col; + set_pad_color(index, COLOR_GREEN); + } + } + // Clear control buttons + for (u8 i = 0; i < GRID_SIZE; i++) { + if ((i / 10 >= 1 && i / 10 <= 8 && i % 10 >= 1 && i % 10 <= 8)) { + continue; // Skip grid pads + } + set_pad_color(i, COLOR_BLACK); + } +} diff --git a/tests/flash_test.c b/tests/flash_test.c index 2511e49..40ac40b 100644 --- a/tests/flash_test.c +++ b/tests/flash_test.c @@ -266,6 +266,64 @@ static void test_flash_all_pages_saved(void **state) { } } +static void test_flash_factory_reset(void **state) { + (void)state; + + // Load and modify settings + flash_load(); + change_page(0); + change_page_root(5); + change_page_scale_type(SCALE_BLUES); + change_page_octave(8); + set_page_midi_channels(0xFF00); + toggle_page_aftertouch_mode(); + set_page_velocity_curve(VELOCITY_CURVE_HARD); + increase_page_transpose(); + + change_page(1); + change_page_root(7); + change_page_octave(2); + + flash_save(); + + // Verify settings were saved + assert_int_equal(get_page(0)->root, 5); + assert_int_equal(get_page(0)->scale_type, SCALE_BLUES); + assert_int_equal(get_page(0)->octave, 8); + + // Perform factory reset + flash_factory_reset(); + + // Verify all pages reset to defaults + for (unsigned char i = 0; i < PAGE_COUNT; i++) { + const Page *page = get_page(i); + assert_int_equal(page->root, 0); + assert_int_equal(page->scale_type, 0); + assert_int_equal(page->octave, 5); + assert_int_equal(page->interval_index, DEFAULT_INTERVAL_INDEX); + assert_int_equal(page->midi_channels, DEFAULT_MIDI_CHANNELS); + assert_int_equal(page->aftertouch_mode, DEFAULT_AFTERTOUCH_MODE); + assert_int_equal(page->velocity_curve, DEFAULT_VELOCITY_CURVE); + assert_int_equal(page->transpose, DEFAULT_TRANSPOSE); + } + + // Verify current page reset to 0 + assert_int_equal(get_current_page(), 0); + + // Verify flash is valid after factory reset + assert_int_equal(flash_is_valid(), 1); + + // Reload from flash and verify defaults persisted + Page empty_pages[PAGE_COUNT] = {0}; + page_manager_set(empty_pages, 0); + flash_load(); + + const Page *page = get_page(0); + assert_int_equal(page->root, 0); + assert_int_equal(page->scale_type, 0); + assert_int_equal(page->octave, 5); +} + int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup(test_flash_load_invalid_magic, setup), @@ -278,6 +336,7 @@ int main(void) { cmocka_unit_test_setup(test_flash_preserves_valid_aftertouch_mode, setup), cmocka_unit_test_setup(test_flash_preserves_valid_transpose, setup), cmocka_unit_test_setup(test_flash_all_pages_saved, setup), + cmocka_unit_test_setup(test_flash_factory_reset, setup), }; return cmocka_run_group_tests(tests, NULL, NULL);