From 50df45fd4eaa043c8c3cb946e32b343716b55395 Mon Sep 17 00:00:00 2001 From: Bas Grolleman Date: Sat, 23 May 2026 10:13:24 +0200 Subject: [PATCH] Optimize --- arduino/cosplay_lights/button.cpp | 103 ---------------------- arduino/cosplay_lights/button.h | 34 ------- arduino/cosplay_lights/config.h | 2 +- arduino/cosplay_lights/cosplay_lights.ino | 100 ++++++++------------- arduino/cosplay_lights/lightshow_format.h | 1 + converter/convert_all.py | 30 +++---- 6 files changed, 53 insertions(+), 217 deletions(-) delete mode 100644 arduino/cosplay_lights/button.cpp delete mode 100644 arduino/cosplay_lights/button.h diff --git a/arduino/cosplay_lights/button.cpp b/arduino/cosplay_lights/button.cpp deleted file mode 100644 index 8a1cf79..0000000 --- a/arduino/cosplay_lights/button.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * button.cpp — Debounced button with single-tap, double-tap, and hold detection. - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "button.h" -#include - -// ---- Timing constants -------------------------------------------------- - -static const uint16_t DEBOUNCE_MS = 50; // minimum stable time before accepting a state change -static const uint16_t HOLD_MS = 800; // hold this long to emit BTN_HOLD -static const uint16_t DOUBLE_TAP_MS = 400; // second tap must arrive within this window - -// ---- State ------------------------------------------------------------- - -static uint8_t s_pin; - -// Debounce state -static bool s_raw_state; // last raw digitalRead result -static bool s_debounced; // stable debounced state (true = pressed) -static uint32_t s_debounce_time; // millis() when raw state last changed - -// Tap counting -static uint8_t s_tap_count; // taps accumulated in the current sequence -static uint32_t s_last_release_ms; // millis() of the most recent release - -// Hold detection -static uint32_t s_press_start_ms; // millis() when current press began -static bool s_hold_fired; // true after BTN_HOLD has been emitted for this press - -// ---- Helpers ----------------------------------------------------------- - -// Returns true when the button is physically pressed. -// INPUT_PULLUP means LOW = pressed. -static inline bool pin_is_pressed() { - return digitalRead(s_pin) == LOW; -} - -// ---- Public API -------------------------------------------------------- - -void button_begin(uint8_t pin) { - s_pin = pin; - s_raw_state = false; - s_debounced = false; - s_debounce_time = 0; - s_tap_count = 0; - s_last_release_ms = 0; - s_press_start_ms = 0; - s_hold_fired = false; - pinMode(pin, INPUT_PULLUP); -} - -ButtonEvent button_update() { - uint32_t now = millis(); - bool raw = pin_is_pressed(); - - // ---- Debounce ---- - // Reset the debounce timer whenever the raw reading changes. - if (raw != s_raw_state) { - s_raw_state = raw; - s_debounce_time = now; - } - - // Only update the debounced state after the signal has been stable. - bool prev = s_debounced; - if ((now - s_debounce_time) >= DEBOUNCE_MS) { - s_debounced = raw; - } - - bool just_pressed = ( s_debounced && !prev); - bool just_released = (!s_debounced && prev); - - // ---- Press start ---- - if (just_pressed) { - s_press_start_ms = now; - s_hold_fired = false; - } - - // ---- Hold detection (fires once while the button is held) ---- - if (s_debounced && !s_hold_fired && (now - s_press_start_ms) >= HOLD_MS) { - s_hold_fired = true; - s_tap_count = 0; // discard any pending taps so hold doesn't also trigger a tap - return BTN_HOLD; - } - - // ---- Count taps on each release (only if hold didn't fire) ---- - if (just_released && !s_hold_fired) { - s_tap_count++; - s_last_release_ms = now; - } - - // ---- Resolve tap count after the double-tap window expires ---- - // Wait until the button is up AND the window has passed before deciding. - if (s_tap_count > 0 && !s_debounced && (now - s_last_release_ms) >= DOUBLE_TAP_MS) { - uint8_t taps = s_tap_count; - s_tap_count = 0; - if (taps == 1) return BTN_TAP; - if (taps >= 2) return BTN_DOUBLE_TAP; - } - - return BTN_NONE; -} diff --git a/arduino/cosplay_lights/button.h b/arduino/cosplay_lights/button.h deleted file mode 100644 index 4ad46bf..0000000 --- a/arduino/cosplay_lights/button.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * button.h — Debounced button with single-tap, double-tap, and hold detection. - * - * Wiring: connect one leg of the button to the configured pin, the other to GND. - * Uses INPUT_PULLUP — no external resistor needed. - * - * Timing (adjust in button.cpp if needed): - * DEBOUNCE_MS — ignores bounces shorter than this (default 50ms) - * HOLD_MS — how long to hold before BTN_HOLD fires (default 800ms) - * DOUBLE_TAP_MS — window to catch a second tap (default 400ms) - * - * Note: single taps are confirmed after DOUBLE_TAP_MS of silence so the - * code can tell them apart from the start of a double-tap. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once -#include - -enum ButtonEvent : uint8_t { - BTN_NONE, // nothing happened - BTN_TAP, // one short press, released, no second press within window - BTN_DOUBLE_TAP, // two presses within DOUBLE_TAP_MS of each other - BTN_HOLD, // button held longer than HOLD_MS -}; - -// Initialize the button. Call once in setup(). -// pin: the Arduino pin the button is wired to (other end to GND). -void button_begin(uint8_t pin); - -// Poll for button events. Call once per loop(). -// Returns BTN_NONE most of the time; returns an event exactly once when detected. -ButtonEvent button_update(); diff --git a/arduino/cosplay_lights/config.h b/arduino/cosplay_lights/config.h index e36d619..4a30058 100644 --- a/arduino/cosplay_lights/config.h +++ b/arduino/cosplay_lights/config.h @@ -37,7 +37,7 @@ // Uses INPUT_PULLUP — no external resistor needed. // 1 tap → next show // 2 taps → previous show -// hold → reset to show 0 (blue pulse) +// hold → reset to show 0 (blue breath) #define BUTTON_PIN 2 // -- Active pattern ------------------------------------------ diff --git a/arduino/cosplay_lights/cosplay_lights.ino b/arduino/cosplay_lights/cosplay_lights.ino index f3e8914..6235a69 100644 --- a/arduino/cosplay_lights/cosplay_lights.ino +++ b/arduino/cosplay_lights/cosplay_lights.ino @@ -12,43 +12,23 @@ * 2. Run: make shows * 3. Run: make upload * - * Required library: FastLED (Sketch > Include Library > Manage Libraries) + * Required libraries: FastLED, OneButton (Sketch > Include Library > Manage Libraries) * SPDX-License-Identifier: BSD-2-Clause */ +#include #include "config.h" #include "led_controller.h" -#include "button.h" #include "shows.h" -// Milliseconds between LED updates. 16ms ≈ 60 refreshes/second. -#define UPDATE_INTERVAL_MS 16 - -// ---- Color math -------------------------------------------------------- - -// Interpolate a single 8-bit channel from a to b by ratio t (0.0 – 1.0). -static uint8_t lerp_channel(uint8_t a, uint8_t b, float t) { - return (uint8_t)(a + (b - a) * t); -} - -// Blend two colors: t=0.0 returns 'from', t=1.0 returns 'to'. -static CRGB blend_colors(CRGB from, CRGB to, float t) { - return CRGB( - lerp_channel(from.r, to.r, t), - lerp_channel(from.g, to.g, t), - lerp_channel(from.b, to.b, t) - ); -} - // ---- Playback state ---------------------------------------------------- -static uint8_t s_show_index = 0; // which show is active (index into SHOWS[]) -static ShowDef s_show; // active show, read from PROGMEM once on load -static uint16_t s_step_index = 0; // current step within the active show -static uint32_t s_step_start = 0; // millis() when this step began -static CRGB s_from_color = CRGB::Black; // color at the start of this transition +static uint8_t s_show_index = 0; +static ShowDef s_show; // PROGMEM cache for the active show +static uint16_t s_step_index = 0; +static uint32_t s_step_start = 0; +static CRGB s_from_color = CRGB::Black; -// Load show at 'index', resetting playback to the first step. static void load_show(uint8_t index) { s_show_index = index; s_show = read_show_def(&SHOWS[index]); @@ -57,18 +37,18 @@ static void load_show(uint8_t index) { s_from_color = CRGB::Black; } -// How far through the current step we are (0.0 – 1.0). -// Returns 1.0 immediately for instant steps (duration_ms == 0). -static float step_progress(const Step& step) { - if (step.duration_ms == 0) return 1.0f; - uint32_t elapsed = millis() - s_step_start; - float t = (float)elapsed / step.duration_ms; - return (t < 1.0f) ? t : 1.0f; +// How far through the current step we are (0–255). +// Returns 255 immediately for instant steps (duration_ms == 0). +static uint8_t step_progress(const Step& step, uint32_t now) { + if (step.duration_ms == 0) return 255; + uint32_t elapsed = now - s_step_start; + if (elapsed >= step.duration_ms) return 255; + return (uint8_t)((elapsed * 255UL) / step.duration_ms); } // Complete the current step and advance to the next. // SHOW_LOOP wraps back to step 0; SHOW_SINGLE loads the next show when the last step ends. -static void advance_step(CRGB reached_color) { +static void advance_step(CRGB reached_color, uint32_t now) { s_from_color = reached_color; uint16_t next = s_step_index + 1; if (next >= s_show.length) { @@ -79,44 +59,40 @@ static void advance_step(CRGB reached_color) { next = 0; } s_step_index = next; - s_step_start = millis(); + s_step_start = now; } +// ---- Button ------------------------------------------------------------ + +static OneButton s_button(BUTTON_PIN, true, true); // active-low, enable pullup + // ---- Arduino entry points ---------------------------------------------- void setup() { leds_begin(); - button_begin(BUTTON_PIN); - load_show(0); // start on show 0 — blue breath + load_show(0); + + s_button.setClickMs(400); + s_button.setPressMs(800); + s_button.attachClick([]() { load_show((s_show_index + 1) % SHOW_COUNT); }); + s_button.attachDoubleClick([]() { load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT); }); + s_button.attachLongPressStart([]() { load_show(0); }); } void loop() { - // ---- Button navigation ---- - ButtonEvent evt = button_update(); - if (evt == BTN_TAP) { - // Next show, wrapping around to 0 after the last one. - load_show((s_show_index + 1) % SHOW_COUNT); - } - if (evt == BTN_DOUBLE_TAP) { - // Previous show, wrapping around to the last one from show 0. - load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT); - } - if (evt == BTN_HOLD) { - // Reset to show 0 (blue breath) from any position. - load_show(0); - } + s_button.tick(); - // ---- Playback ---- - Step step = read_step(&s_show.steps[s_step_index]); - CRGB to = CRGB(step.r, step.g, step.b); - float t = step_progress(step); + static uint32_t s_last_frame = 0; + uint32_t now = millis(); + if (now - s_last_frame < 16) return; + s_last_frame = now; - leds_apply_color(blend_colors(s_from_color, to, t)); + Step step = read_step(&s_show.steps[s_step_index]); + CRGB to = CRGB(step.r, step.g, step.b); + uint8_t t8 = step_progress(step, now); + + leds_apply_color(blend(s_from_color, to, t8)); leds_show(); - if (t >= 1.0f) { - advance_step(to); - } - - delay(UPDATE_INTERVAL_MS); + if (t8 == 255) advance_step(to, now); } diff --git a/arduino/cosplay_lights/lightshow_format.h b/arduino/cosplay_lights/lightshow_format.h index c40337a..8a718fc 100644 --- a/arduino/cosplay_lights/lightshow_format.h +++ b/arduino/cosplay_lights/lightshow_format.h @@ -49,3 +49,4 @@ inline ShowDef read_show_def(const ShowDef* ptr) { sd.mode = pgm_read_byte(&ptr->mode); return sd; } +` \ No newline at end of file diff --git a/converter/convert_all.py b/converter/convert_all.py index 1949362..e7fc00a 100644 --- a/converter/convert_all.py +++ b/converter/convert_all.py @@ -10,7 +10,7 @@ Usage: python converter/convert_all.py (or via Makefile: make shows) -Show 0 is always 'blue_pulse' — the home/reset show. +Show 0 is always 'blue_breath' — the home/reset show. All other shows are sorted alphabetically and follow after it. SPDX-License-Identifier: BSD-2-Clause @@ -36,16 +36,6 @@ STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)') # ---- Helpers ----------------------------------------------------------- -def parse_mode(filepath: Path) -> str: - """Return the C mode constant for this show. Defaults to SHOW_LOOP.""" - with open(filepath) as f: - for line in f: - m = re.match(r'^\s*//\s*mode:\s*(\w+)', line, re.IGNORECASE) - if m: - return "SHOW_SINGLE" if m.group(1).lower() == "single" else "SHOW_LOOP" - return "SHOW_LOOP" - - def filename_to_symbol(stem: str) -> str: """Convert a filename stem to an uppercase C array symbol. e.g. 'example_fade' → 'SHOW_EXAMPLE_FADE'""" clean = re.sub(r'[^a-zA-Z0-9]', '_', stem) @@ -56,14 +46,21 @@ def hex_to_rgb(hex_str: str) -> tuple[int, int, int]: return int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16) -def parse_show(filepath: Path) -> list[tuple[int, int, int, int]]: +def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str]: """ - Parse a .txt show file. Returns list of (r, g, b, duration_ms) tuples. - Raises ValueError on malformed lines. + Parse a .txt show file in one pass. Returns (steps, mode_constant). + steps: list of (r, g, b, duration_ms) tuples + mode_constant: 'SHOW_LOOP' or 'SHOW_SINGLE' (default SHOW_LOOP if not set) + Raises ValueError on malformed lines or empty files. """ steps = [] + mode = "SHOW_LOOP" with open(filepath) as f: for lineno, raw_line in enumerate(f, 1): + m = re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE) + if m: + mode = "SHOW_SINGLE" if m.group(1).lower() == "single" else "SHOW_LOOP" + continue line = raw_line.split("//")[0].strip() if not line: continue @@ -78,7 +75,7 @@ def parse_show(filepath: Path) -> list[tuple[int, int, int, int]]: steps.append((r, g, b, duration)) if not steps: raise ValueError(f"{filepath.name}: file contains no steps.") - return steps + return steps, mode def render_show_header(steps: list, source_name: str, symbol: str) -> str: @@ -166,9 +163,8 @@ def main() -> None: for txt_path in ordered_files: stem = txt_path.stem symbol = filename_to_symbol(stem) - mode = parse_mode(txt_path) try: - steps = parse_show(txt_path) + steps, mode = parse_show_file(txt_path) header = render_show_header(steps, txt_path.name, symbol) out = SKETCH_DIR / f"show_{stem}.h" out.write_text(header, encoding="utf-8")