butt-head
A no_std Rust library for processing button inputs in embedded systems. Transforms clean boolean pin states into button gesture events through a configurable state machine. Pure logic — no I/O, no HAL, no interrupts. The button counterpart to pot-head.
Scope
butt-head handles gesture recognition: single and multi-click detection, hold detection, and multi-click sequences. It operates on clean boolean pin states and does not perform debouncing. Debounce is a hardware-level concern that depends on sampling rate and electrical characteristics — it belongs in your HAL or input driver, before the state reaches this library.
What You Get
Feed in a pin state and a timestamp. Get back a structured event and a hint for when to call again.
let result = button.update;
match result.event
Match on exactly the gestures you care about and ignore the rest. All gesture semantics are handled for you:
- A double-click fires a single
Click { count: 2 }— never two separate single-clicks. - A long press fires
Holdevents — never aClick. - Click-then-hold is distinguished from plain hold via
clicks_before.
For multi-button combos, is_pressed() and pressed_duration(now) let you query button state directly without waiting for an event.
Configuration
static CONFIG: = Config ;
let mut button = new;
Config lives as a &'static reference — zero runtime overhead, sits in flash on embedded targets.
max_click_count lets you short-circuit the click_timeout wait:
None— always wait forclick_timeoutto expire before emitting (default).Some(1)— emitClickon every release immediately, with no timeout wait.Some(n)— emit immediately once the n-th click in a sequence lands.
Events
| Event | When it fires |
|---|---|
Press |
Immediately on every press edge |
Release { duration } |
Immediately on every release edge |
Click { count } |
After click_timeout with no further press, or immediately when max_click_count is reached; count reflects multi-clicks |
Hold { clicks_before, level } |
Repeatedly while held; level increments on each repeat |
Power-Efficient Scheduling
Every call to update() returns a ServiceTiming hint telling you exactly when to call again:
match result.next_service
During idle your firmware sleeps until a pin interrupt fires. During a gesture the timer wakes you up at the exact moment the next event could fire. No polling loops, no wasted CPU cycles.
Works Everywhere
butt-head is HAL-agnostic. Integrate it by implementing two small traits — TimeDuration and TimeInstant — for your platform's time types. See examples/ for complete integrations with std::time, STM32 SysTick, and Embassy.
Feature Flags
| Feature | What it enables |
|---|---|
defmt |
defmt::Format on all public types for structured RTT logging |
Examples
| Example | Description |
|---|---|
examples/native |
Desktop demo using std::time and crossterm |
examples/stm32f0 |
Bare-metal STM32F0 with SysTick timer |
examples/stm32f0-embassy |
STM32F0 with Embassy async/await |
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.