Optimize
This commit is contained in:
@@ -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 <Arduino.h>
|
|
||||||
|
|
||||||
// ---- 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;
|
|
||||||
}
|
|
||||||
@@ -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 <stdint.h>
|
|
||||||
|
|
||||||
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();
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
// Uses INPUT_PULLUP — no external resistor needed.
|
// Uses INPUT_PULLUP — no external resistor needed.
|
||||||
// 1 tap → next show
|
// 1 tap → next show
|
||||||
// 2 taps → previous show
|
// 2 taps → previous show
|
||||||
// hold → reset to show 0 (blue pulse)
|
// hold → reset to show 0 (blue breath)
|
||||||
#define BUTTON_PIN 2
|
#define BUTTON_PIN 2
|
||||||
|
|
||||||
// -- Active pattern ------------------------------------------
|
// -- Active pattern ------------------------------------------
|
||||||
|
|||||||
@@ -12,43 +12,23 @@
|
|||||||
* 2. Run: make shows
|
* 2. Run: make shows
|
||||||
* 3. Run: make upload
|
* 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
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <OneButton.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "led_controller.h"
|
#include "led_controller.h"
|
||||||
#include "button.h"
|
|
||||||
#include "shows.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 ----------------------------------------------------
|
// ---- Playback state ----------------------------------------------------
|
||||||
|
|
||||||
static uint8_t s_show_index = 0; // which show is active (index into SHOWS[])
|
static uint8_t s_show_index = 0;
|
||||||
static ShowDef s_show; // active show, read from PROGMEM once on load
|
static ShowDef s_show; // PROGMEM cache for the active show
|
||||||
static uint16_t s_step_index = 0; // current step within the active show
|
static uint16_t s_step_index = 0;
|
||||||
static uint32_t s_step_start = 0; // millis() when this step began
|
static uint32_t s_step_start = 0;
|
||||||
static CRGB s_from_color = CRGB::Black; // color at the start of this transition
|
static CRGB s_from_color = CRGB::Black;
|
||||||
|
|
||||||
// Load show at 'index', resetting playback to the first step.
|
|
||||||
static void load_show(uint8_t index) {
|
static void load_show(uint8_t index) {
|
||||||
s_show_index = index;
|
s_show_index = index;
|
||||||
s_show = read_show_def(&SHOWS[index]);
|
s_show = read_show_def(&SHOWS[index]);
|
||||||
@@ -57,18 +37,18 @@ static void load_show(uint8_t index) {
|
|||||||
s_from_color = CRGB::Black;
|
s_from_color = CRGB::Black;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How far through the current step we are (0.0 – 1.0).
|
// How far through the current step we are (0–255).
|
||||||
// Returns 1.0 immediately for instant steps (duration_ms == 0).
|
// Returns 255 immediately for instant steps (duration_ms == 0).
|
||||||
static float step_progress(const Step& step) {
|
static uint8_t step_progress(const Step& step, uint32_t now) {
|
||||||
if (step.duration_ms == 0) return 1.0f;
|
if (step.duration_ms == 0) return 255;
|
||||||
uint32_t elapsed = millis() - s_step_start;
|
uint32_t elapsed = now - s_step_start;
|
||||||
float t = (float)elapsed / step.duration_ms;
|
if (elapsed >= step.duration_ms) return 255;
|
||||||
return (t < 1.0f) ? t : 1.0f;
|
return (uint8_t)((elapsed * 255UL) / step.duration_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the current step and advance to the next.
|
// 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.
|
// 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;
|
s_from_color = reached_color;
|
||||||
uint16_t next = s_step_index + 1;
|
uint16_t next = s_step_index + 1;
|
||||||
if (next >= s_show.length) {
|
if (next >= s_show.length) {
|
||||||
@@ -79,44 +59,40 @@ static void advance_step(CRGB reached_color) {
|
|||||||
next = 0;
|
next = 0;
|
||||||
}
|
}
|
||||||
s_step_index = next;
|
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 ----------------------------------------------
|
// ---- Arduino entry points ----------------------------------------------
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
leds_begin();
|
leds_begin();
|
||||||
button_begin(BUTTON_PIN);
|
load_show(0);
|
||||||
load_show(0); // start on show 0 — blue breath
|
|
||||||
|
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() {
|
void loop() {
|
||||||
// ---- Button navigation ----
|
s_button.tick();
|
||||||
ButtonEvent evt = button_update();
|
|
||||||
if (evt == BTN_TAP) {
|
static uint32_t s_last_frame = 0;
|
||||||
// Next show, wrapping around to 0 after the last one.
|
uint32_t now = millis();
|
||||||
load_show((s_show_index + 1) % SHOW_COUNT);
|
if (now - s_last_frame < 16) return;
|
||||||
}
|
s_last_frame = now;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Playback ----
|
|
||||||
Step step = read_step(&s_show.steps[s_step_index]);
|
Step step = read_step(&s_show.steps[s_step_index]);
|
||||||
CRGB to = CRGB(step.r, step.g, step.b);
|
CRGB to = CRGB(step.r, step.g, step.b);
|
||||||
float t = step_progress(step);
|
uint8_t t8 = step_progress(step, now);
|
||||||
|
|
||||||
leds_apply_color(blend_colors(s_from_color, to, t));
|
leds_apply_color(blend(s_from_color, to, t8));
|
||||||
leds_show();
|
leds_show();
|
||||||
|
|
||||||
if (t >= 1.0f) {
|
if (t8 == 255) advance_step(to, now);
|
||||||
advance_step(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
delay(UPDATE_INTERVAL_MS);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,3 +49,4 @@ inline ShowDef read_show_def(const ShowDef* ptr) {
|
|||||||
sd.mode = pgm_read_byte(&ptr->mode);
|
sd.mode = pgm_read_byte(&ptr->mode);
|
||||||
return sd;
|
return sd;
|
||||||
}
|
}
|
||||||
|
`
|
||||||
+13
-17
@@ -10,7 +10,7 @@ Usage:
|
|||||||
python converter/convert_all.py
|
python converter/convert_all.py
|
||||||
(or via Makefile: make shows)
|
(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.
|
All other shows are sorted alphabetically and follow after it.
|
||||||
|
|
||||||
SPDX-License-Identifier: BSD-2-Clause
|
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 -----------------------------------------------------------
|
# ---- 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:
|
def filename_to_symbol(stem: str) -> str:
|
||||||
"""Convert a filename stem to an uppercase C array symbol. e.g. 'example_fade' → 'SHOW_EXAMPLE_FADE'"""
|
"""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)
|
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)
|
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.
|
Parse a .txt show file in one pass. Returns (steps, mode_constant).
|
||||||
Raises ValueError on malformed lines.
|
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 = []
|
steps = []
|
||||||
|
mode = "SHOW_LOOP"
|
||||||
with open(filepath) as f:
|
with open(filepath) as f:
|
||||||
for lineno, raw_line in enumerate(f, 1):
|
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()
|
line = raw_line.split("//")[0].strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
@@ -78,7 +75,7 @@ def parse_show(filepath: Path) -> list[tuple[int, int, int, int]]:
|
|||||||
steps.append((r, g, b, duration))
|
steps.append((r, g, b, duration))
|
||||||
if not steps:
|
if not steps:
|
||||||
raise ValueError(f"{filepath.name}: file contains no 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:
|
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:
|
for txt_path in ordered_files:
|
||||||
stem = txt_path.stem
|
stem = txt_path.stem
|
||||||
symbol = filename_to_symbol(stem)
|
symbol = filename_to_symbol(stem)
|
||||||
mode = parse_mode(txt_path)
|
|
||||||
try:
|
try:
|
||||||
steps = parse_show(txt_path)
|
steps, mode = parse_show_file(txt_path)
|
||||||
header = render_show_header(steps, txt_path.name, symbol)
|
header = render_show_header(steps, txt_path.name, symbol)
|
||||||
out = SKETCH_DIR / f"show_{stem}.h"
|
out = SKETCH_DIR / f"show_{stem}.h"
|
||||||
out.write_text(header, encoding="utf-8")
|
out.write_text(header, encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user