Hybrid evaluation of exact full-release combos and tap-while-hold gestures. Includes debounced input masks, a short coalescing window for near-simultaneous presses, and optional per-entry conditions.
Author: Björn Gaebel · Date: 2025-09-11 · License: MIT
- Full-release combos (exact): Execute once when all buttons are released. If multiple thresholds exist for the same mask, the largest satisfied
minDurationwins. - Tap-while-hold: While an anchor (
holdMask) is fully pressed, detect each press+release of atapMaskand execute an action per tap (supports repeated taps). - Optional conditions: Gate any entry with
COND(...), e.g.COND(machineState),COND(!machineState), or arbitrary boolean expressions. - Stable input: Built-in debounce (
BTN_DEBOUNCE_MS) and initial coalescing (COMBO_COALESCE_MS) to treat near-simultaneous presses as one combo. - No accidental singles after combos: A single-key full-release can be suppressed if a multi-press occurred in the same session — valid multi-mask full-release still fires.
ButtonTransitions.h– API, types, and macrosButtonTransitions.cpp– implementation
Include both in your project and provide this hook:
uint8_t getButtonMask(void)
{
// Return current button bitmask (bit i => Si pressed), or 0 if none.
}Initialize and call the state machine regularly:
void setup()
{
#if defined(DEBUG)
Serial.begin(500000);
delay(200);
Serial.println(F("DEBUG ENABLED"));
#endif
transitionsInit();
}
void loop()
{
processTransitionsHybrid(tableEx, NUM_ENTRIES, millis());
}Style: Opening braces on a new line. English names like
cntCtrl,lastMeasurement,newMeasurement. Prefer a clear state-machine structure.
Use the macros from the header. Conditions are optional via COND(...) (a capture-less lambda that decays to bool(*)()).
extern bool machineState;
extern int cntCtrl;
// Actions
void startAutoMode()
{
// ...
}
void incCounter()
{
// ...
}
// Table
ButtonTransitionEx tableEx[] =
{
// Full-release without condition (exact mask, min duration in ms)
FULL_RELEASE(BTN_TOP_LEFT | BTN_TOP_RIGHT, 2000, ModeAuto, startAutoMode),
// Full-release with condition (machineState must be true)
FULL_RELEASE(BTN_TOP_LEFT | BTN_TOP_RIGHT, 2000, ModeAuto, COND(machineState), startAutoMode),
// Tap-while-hold: hold TL, tap TR between 30..250 ms (repeatable)
TAP_WHILE_HOLD(BTN_TOP_LEFT, BTN_TOP_RIGHT, 30, 250, ModeNoChange, incCounter),
// Tap-while-hold with condition (only if counter > 0)
TAP_WHILE_HOLD(BTN_TOP_LEFT, BTN_BOTTOM_LEFT, 50, 0, ModeNoChange, COND(cntCtrl > 0), incCounter)
};
constexpr size_t NUM_ENTRIES = sizeof(tableEx) / sizeof(tableEx[0]);Policies
- Full-release: exact mask must match what was held until release; the entry with the greatest satisfied
minDurationis chosen. - Tap-while-hold: exact
holdMaskmust remain pressed; exacttapMaskmust be pressed then released;tapMin ≤ dur ≤ tapMax(ortapMax == 0for open upper bound).
| Macro | Meaning | Default |
|---|---|---|
BTN_DEBOUNCE_MS |
Debounce commit for the mask level | 20 ms |
COMBO_COALESCE_MS |
Group near-simultaneous initial presses | 30 ms |
BTNTRANS_NO_NEXTMODE |
Define to omit nextMode handling |
(unset) |
Tweak these in a project config header or via build flags.
// Reset all internal state at startup
void transitionsInit(void);
// Main evaluation; call each loop/tick
void processTransitionsHybrid(struct ButtonTransitionEx* tableEx, size_t num, uint32_t now);
// Drop current press/tap context (e.g., when changing app context)
void transitionsReset(void);
// Provided by your app: read the current button mask (bit i => Si pressed)
uint8_t getButtonMask(void);Types & Macros (from the header):
enum OperationMode { ModeNoChange=-1, ModeBasic, ModeAuto };enum ComboEvalType { ComboEvalOnFullRelease, ComboEvalTapWhileHold };struct ButtonTransitionEx { ... }(supports.condpredicate)FULL_RELEASE(...),TAP_WHILE_HOLD(...), andCOND(expr)
Define DEBUG globally (e.g., PlatformIO build_flags = -DDEBUG) and ensure Serial.begin(...) in setup().
You’ll see messages like:
[BTN] pressed: mask=0x03 (S0+S1)
Tap while hold: end
Full-release exec
This project is licensed under the MIT License.
© 2025 Björn Gaebel. See source headers for full text.