Expand description
§debounce-button-eng
debounce-button-eng is a no_std crate for handling a mechanical button
through embedded-hal::digital::InputPin.
The crate is not tied to a specific microcontroller or timer. The application passes monotonic time in milliseconds, while the crate filters contact bounce and returns the stable button state plus short event flags.
§Features
- configurable debounce time;
active-lowandactive-highbuttons;- stable
Pressed/Releasedstate; pressedandreleasedevents;- single click;
- double click;
- configurable multi-click sequence, for example 5 short clicks;
- hold event;
- current and final hold duration;
- tests based on
embedded-hal-mock.
§Documentation
API documentation is available on docs.rs: https://docs.rs/debounce-button-eng.
§Installation
For local use as a path dependency:
[dependencies]
debounce-button-eng = { path = "../debounce-button-eng" }If the crate is published to crates.io, the dependency can be written by version:
[dependencies]
debounce-button-eng = "0.2"§Quick Start
A typical button is wired between a GPIO pin and ground while the input uses a
pull-up resistor. In that case the pressed state is logical Low, so use
ButtonConfig::active_low().
use debounce_button_eng::{Button, ButtonConfig};
let config = ButtonConfig::active_low()
.with_debounce_ms(20)
.with_double_click_ms(300)
.with_hold_ms(1_000)
.with_multi_click(5, 3_000);
let mut button = Button::with_config(input_pin, config);
loop {
let now_ms = monotonic_millis();
let update = button.update(now_ms)?;
if update.events.pressed {
// The button was pressed.
}
if update.events.released {
// The button was released.
}
if update.events.click {
// A single click was confirmed.
}
if update.events.double_click {
// A double click was completed.
}
if update.events.multi_click {
// The configured number of short clicks was completed.
}
if update.events.hold {
// The button was held for at least hold_ms.
}
}input_pin must implement embedded_hal::digital::InputPin.
§Time
Button::update(now_ms) accepts monotonic time in milliseconds. Monotonic time
is a counter that only moves forward, for example milliseconds since MCU boot.
Correct:
button.update(100)?;
button.update(105)?;
button.update(120)?;Incorrect:
button.update(100)?;
button.update(80)?; // time moved backwardsThe crate uses differences between timestamps to determine:
- how long the input has been stable after a raw change;
- when a new state can be accepted after debounce;
- how long the button has been held;
- whether the second click landed inside the double-click window;
- whether the next short click arrived before the multi-click timeout expired.
§Configuration
let config = ButtonConfig::active_low()
.with_debounce_ms(20)
.with_double_click_ms(300)
.with_hold_ms(1_000)
.with_multi_click(5, 3_000);Configuration fields:
debounce_ms- how many milliseconds the raw pin state must remain stable before it becomes the debounced state;double_click_ms- maximum interval between two short clicks;hold_ms- hold threshold;multi_click_count- how many short clicks are required forevents.multi_click;multi_click_timeout_ms- maximum pause between short clicks in one multi-click sequence;active_level- electrical level that represents a pressed button.
multi_click_count values 0 and 1 disable the multi-click detector. The
default is disabled, so existing click and double_click behavior does not
change unless you opt in.
For a button that is pressed when the input is high:
let config = ButtonConfig::active_high();Timings can be changed at runtime:
button.set_debounce_ms(30);
button.set_double_click_ms(250);
button.set_hold_ms(1_500);
button.set_multi_click_count(5);
button.set_multi_click_timeout_ms(3_000);§Events
Button::update returns ButtonUpdate:
let update = button.update(now_ms)?;Main fields:
update.state- current stable button state;update.events.pressed- the stable state changed toPressed;update.events.released- the stable state changed toReleased;update.events.click- a single short click was confirmed after thedouble_click_mswindow expired;update.events.double_click- two short clicks completed inside thedouble_click_mswindow;update.events.multi_click- the configured number of short clicks completed before the pause between clicks exceededmulti_click_timeout_ms;update.events.hold- the hold threshold was reached;update.held_ms- how many milliseconds the button is currently held;update.released_after_ms- how many milliseconds the button was held before it was released.update.multi_click_count- current short-click count in the active multi-click sequence. Whenevents.multi_clickis true, this value is the configured target count that fired the event.
events.hold fires once per hold. If you need the continuously updated hold
time, read held_ms.
events.click is intentionally delayed until the double_click_ms window
expires. Without this delay, a double click would first emit a single click
and then a double_click. The first short release is now stored as a pending
click. If the second short click arrives in time, only double_click is
emitted. If no second click arrives, the pending click becomes click.
§Multi-click Sequences
Use with_multi_click(count, timeout_ms) when an action should run only after a
specific number of short clicks:
let config = ButtonConfig::active_low()
.with_debounce_ms(20)
.with_hold_ms(1_000)
.with_multi_click(5, 3_000);With this configuration, every short release increments the internal
multi-click counter. When the fifth short release is accepted before a pause
greater than 3_000 ms appears between releases, update.events.multi_click
is true for that update and update.multi_click_count is 5.
If the pause between two short releases is greater than
multi_click_timeout_ms, the counter is reset. A long hold is not counted as a
short click and also clears the active multi-click sequence.
For a configurable double click that allows a longer pause than
double_click_ms, use a target count of 2:
let config = ButtonConfig::active_low()
.with_double_click_ms(250)
.with_multi_click(2, 3_000);In this mode, events.multi_click can represent a slow double click within
three seconds, while events.double_click still keeps its stricter
double_click_ms meaning. While a multi-click sequence is active, the delayed
single click is held back until the sequence either completes or times out.
§How Button::update Works
update(now_ms) is the crate’s main function. Call it regularly from the main
loop, an RTOS task, or a timer. Each call reads the pin once, updates internal
state, and returns ButtonUpdate.
let update = button.update(now_ms)?;Important: update does not wait internally. It does not call delay and does
not block the program during debounce. Filtering happens across repeated calls
with fresh now_ms values.
Internal state:
raw_state- latest physical pin state after applyingactive_loworactive_high;raw_changed_at- time whenraw_statelast changed;stable_state- state that has already passed debounce filtering;pressed_at- time when the stable state becamePressed;pending_click_at- time of a short release that is waiting to become either a singleclickor part of adouble_click;multi_click_seen- number of short clicks in the active multi-click sequence;multi_click_last_at- time of the latest short release in that sequence;hold_reported- whetherholdhas already been emitted for the current hold.
One update(now_ms) call works like this:
-
Read the pin through
pin.is_high(). -
Convert the electrical level to
ButtonState. Foractive_low:Low -> Pressed,High -> Released. -
If the new raw state differs from the previous
raw_state, store it and restart the debounce timer:raw_state = new state raw_changed_at = now_msNo
pressedorreleasedevent is created yet. -
If
raw_statediffers fromstable_state, check how long the raw state has stayed unchanged:now_ms - raw_changed_at >= debounce_ms -
Only then is the raw state accepted as the new stable state. This is where
pressedorreleasedevents appear. -
When the stable state becomes
Pressed, storepressed_at = now_ms. -
When the stable state becomes
Released, computereleased_after_ms = now_ms - pressed_at. -
If the release happened before
hold_ms, it is a short-click candidate. -
If multi-click detection is enabled, increment the active sequence count. If the pause since the previous short release is greater than
multi_click_timeout_ms, reset the count before incrementing it. -
If the sequence reaches
multi_click_count, emitevents.multi_click, report the target count inupdate.multi_click_count, and clear the sequence. -
Store the short release in
pending_click_at;events.clickis not emitted yet while the click can still become part of a double click or an active multi-click sequence. -
If a second short click completes within
double_click_ms, emitevents.double_clickand clear the pending single click. -
If the second click does not arrive before
double_click_msexpires and no multi-click sequence is active, emitevents.click. -
While the button remains stably pressed, compute
held_ms. -
When
held_ms >= hold_ms, emitevents.holdonce for the hold.
Example with debounce_ms = 20, active-low button:
now_ms pin raw_state stable_state event
0 High Released Released -
10 Low Pressed Released raw changed, wait for debounce_ms
25 Low Pressed Released only 15 ms elapsed, too early
30 Low Pressed Pressed events.pressed = true
80 High Released Pressed raw changed, wait for debounce_ms
95 High Released Pressed only 15 ms elapsed, too early
100 High Released Released events.released = true, click waits for double_click_ms
301 High Released Released events.click = trueThe important rule is that an event is not produced when the physical pin first
changes. It is produced only after the new state has remained stable for at
least debounce_ms.
ButtonUpdate describes the current call only:
statestores the current stable state and persists between calls;events.*are short one-call pulses;held_msis present only while the button is stably pressed;released_after_msis present only in the call that stably released the button.multi_click_countshows the active multi-click sequence length, or the target count on the update that emitsevents.multi_click.
If pin.is_high() returns an error, update returns Err(...) immediately.
Internal state is not updated in that case.
Calling update more often gives more precise event timing. A 1-10 ms polling
period is usually convenient. Slower polling still keeps the logic correct, but
events can be delayed by up to the polling period.
§Function Call Flow
The main path starts in the user loop that regularly calls
Button::update(now_ms).
flowchart TD
user["User loop"] --> update["Button::update(now_ms)"]
update --> read_raw["read_raw_state()"]
read_raw --> pin_read["pin.is_high()"]
read_raw --> map_level["ActiveLevel::state_from_high(...)"]
update --> raw_changed{"raw_state changed?"}
raw_changed -->|yes| save_raw["raw_state = new state\nraw_changed_at = now_ms"]
raw_changed -->|no| debounce_check
save_raw --> debounce_check{"elapsed(now_ms, raw_changed_at)\n>= debounce_ms?"}
debounce_check -->|yes| accept["accept_stable_state(now_ms, update)"]
debounce_check -->|no| hold_path
accept --> pressed_or_released{"new stable_state"}
pressed_or_released -->|Pressed| accept_pressed["pressed_at = now_ms\nhold_reported = false\nevents.pressed = true"]
pressed_or_released -->|Released| accept_released["released_after_ms = elapsed(...)\nevents.released = true"]
accept_released --> short_press{"held_ms < hold_ms?"}
short_press -->|yes| register_short_release["register_short_release(now_ms, update)"]
short_press -->|no| hold_path
register_short_release --> multi_count["register_multi_click(...)\nreset count if pause > timeout"]
multi_count --> multi_target{"multi_click_count reached?"}
multi_target -->|yes| multi_event["events.multi_click = true\nclear multi-click count"]
multi_target -->|no| double_check
multi_event --> double_check{"pending_click_at inside\ndouble_click_ms?"}
double_check -->|yes| double_event["events.double_click = true\npending_click_at = None"]
double_check -->|no| multi_fired{"multi_click fired?"}
multi_fired -->|yes| clear_pending["pending_click_at = None"]
multi_fired -->|no| remember_click["pending_click_at = Some(now_ms)"]
remember_click --> pending_check{"pending_click_at expired?"}
pending_check -->|yes| click_event["events.click = true\npending_click_at = None"]
pending_check -->|no| hold_path
accept_pressed --> hold_path
clear_pending --> hold_path
click_event --> hold_path
double_event --> hold_path
hold_path{"stable_state == Pressed?"} -->|yes| held["held_ms(now_ms)"]
held --> elapsed_held["elapsed(now_ms, pressed_at)"]
elapsed_held --> hold_check{"held_ms >= hold_ms\nand hold not reported?"}
hold_check -->|yes| hold_event["events.hold = true\nhold_reported = true"]
hold_check -->|no| result
hold_path -->|no| result["Ok(ButtonUpdate)"]
hold_event --> resultThe same flow as a list:
Button::update(now_ms)reads the physical pin throughread_raw_state().read_raw_state()callspin.is_high()and maps the level toButtonStatethroughActiveLevel::state_from_high(...).- If the raw state changed,
updatestores the new raw state andraw_changed_at. - If the raw state has been stable for at least
debounce_ms,accept_stable_state(...)is called. - A transition to
Pressedemitsevents.pressedand storespressed_at. - A transition to
Releasedemitsevents.released, computesreleased_after_ms, and callsregister_short_release(...)for a short press. register_short_release(...)updates the multi-click counter when the detector is enabled.- If the counter reaches
multi_click_count,events.multi_clickis emitted and the multi-click counter is cleared. register_short_release(...)also confirmsevents.double_clickif the previous short release was insidedouble_click_ms, or stores a new pending single click.- If the pending single click lives longer than
double_click_msand no multi-click sequence is active,updateemitsevents.click. - If the stable state is currently
Pressed,updatecomputesheld_ms. - When
held_ms >= hold_ms,events.holdis emitted once for the hold.
Constructors and configuration:
flowchart LR
new["Button::new(pin)"] --> with_config["Button::with_config(pin, ButtonConfig::active_low())"]
custom["Button::with_config(pin, config)"] --> button["Button<PIN>"]
with_config --> button
active_low["ButtonConfig::active_low()"] --> config["ButtonConfig"]
active_high["ButtonConfig::active_high()"] --> active_low
debounce["with_debounce_ms(...)"] --> config
double["with_double_click_ms(...)"] --> config
hold["with_hold_ms(...)"] --> config
multi_count["with_multi_click_count(...)"] --> config
multi_timeout["with_multi_click_timeout_ms(...)"] --> config
multi["with_multi_click(...)"] --> config§Tests
Run library tests:
cargo testCheck formatting and clippy:
cargo fmt --check
cargo clippy --all-targets -- -D warningsStructs§
- Button
- Debounced wrapper around an
embedded-halinput pin. - Button
Config - Button timing and polarity configuration.
- Button
Events - Events produced by one
Button::updatecall. - Button
Update - Result of one button update.
Enums§
- Active
Level - Electrical level that represents a pressed button.
- Button
State - Stable button state after debounce filtering.
Type Aliases§
- Millis
- Monotonic time in milliseconds.