# debounce-button-matrix-eng

`debounce-button-matrix-eng` is a `no_std` crate for scanning a rectangular
button matrix through `embedded-hal` GPIO traits.

The crate is based on the simple debounced state machine from
`debounce-button-eng`, but keeps only matrix-related basics:

- debounced `Pressed` / `Released` state for every key;
- press and release events;
- hold duration while a key is pressed;
- final hold duration on release;
- multiple key events returned from one scan;
- user-defined key names returned with every event;
- no heap allocation.

## GPIO Model

A matrix is passed as two arrays:

- `rows: [ROW; ROWS]`, where `ROW: embedded_hal::digital::InputPin`;
- `columns: [COL; COLS]`, where `COL: embedded_hal::digital::OutputPin`.

Every scan selects one column output, reads all row inputs, updates the
debouncer for each key in that column, deselects the column, and moves to the
next one.

The common pull-up wiring is:

- rows are inputs with pull-up resistors;
- columns are outputs;
- inactive columns are high;
- the currently scanned column is driven low;
- a pressed key makes the row read low.

That is the default `MatrixConfig::active_low()` mode.

For pull-down style wiring, use `MatrixConfig::active_high()`.

## Key Names

Each key has a user-provided name:

```rust,ignore
let names = [
    ["1", "2", "3"],
    ["4", "5", "6"],
    ["7", "8", "9"],
    ["*", "0", "#"],
];
```

The name type only needs to be `Copy`. For production firmware, an enum is
often better than strings:

```rust,ignore
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Key {
    Up,
    Down,
    Left,
    Right,
}

let names = [
    [Key::Up, Key::Down],
    [Key::Left, Key::Right],
];
```

Every returned event contains `event.name`, `event.row`, and `event.column`.

## Quick Start

```rust,ignore
use debounce_button_matrix_eng::{ButtonEventKind, ButtonMatrix, MatrixConfig};

let rows = [row0, row1, row2, row3];
let columns = [col0, col1, col2];
let names = [
    ["1", "2", "3"],
    ["4", "5", "6"],
    ["7", "8", "9"],
    ["*", "0", "#"],
];

let config = MatrixConfig::active_low().with_debounce_ms(20);
let mut matrix = ButtonMatrix::with_config(rows, columns, names, config);

loop {
    let now_ms = monotonic_millis();
    let events = matrix.scan(now_ms)?;

    for event in events.iter() {
        match event.kind {
            ButtonEventKind::Pressed => {
                // event.name was pressed.
            }
            ButtonEventKind::Released => {
                // event.name was released.
                // event.held_ms contains the debounced hold duration.
            }
        }
    }

    for key in matrix.pressed_keys(now_ms) {
        // key.name is currently pressed.
        // key.held_ms is continuously updated hold duration.
    }
}
```

## Passing GPIO from esp-hal

The exact type names depend on the `esp-hal` version and chip, but the shape is
the same: create input pins for rows, output pins for columns, then pass arrays
to the matrix.

```rust,ignore
let rows = [
    Input::new(peripherals.GPIO1, Pull::Up),
    Input::new(peripherals.GPIO2, Pull::Up),
    Input::new(peripherals.GPIO3, Pull::Up),
    Input::new(peripherals.GPIO4, Pull::Up),
];

let columns = [
    Output::new(peripherals.GPIO5, Level::High),
    Output::new(peripherals.GPIO6, Level::High),
    Output::new(peripherals.GPIO7, Level::High),
];

let names = [
    ["1", "2", "3"],
    ["4", "5", "6"],
    ["7", "8", "9"],
    ["*", "0", "#"],
];

let mut matrix = ButtonMatrix::new(rows, columns, names);
```

For an active-low matrix, initialize column outputs high. `scan()` will drive one
column low at a time and return it high before selecting the next column.

## Multiple Keys

Each key has an independent debounce state. If two keys become stable during
the same scan, both events are returned:

```rust,ignore
let events = matrix.scan(now_ms)?;

for event in events.iter() {
    // Handles all events from this scan, not only the first one.
}
```

The crate can track multiple pressed keys in software. For arbitrary multi-key
combinations in real hardware, the matrix should use diodes. Without diodes,
some combinations can create ghost keys electrically before software sees the
signals.

## Examples

### Short and long presses

Use the release event's `held_ms` to distinguish a tap from a long press. The
duration starts when the press becomes debounced, so it does not include the
initial debounce interval.

```rust,ignore
use debounce_button_matrix_eng::ButtonEventKind;

const LONG_PRESS_MS: u64 = 750;

for event in matrix.scan(now_ms)?.iter() {
    match (event.name, event.kind, event.held_ms) {
        (Key::Enter, ButtonEventKind::Released, Some(held_ms))
            if held_ms >= LONG_PRESS_MS =>
        {
            open_settings();
        }
        (Key::Enter, ButtonEventKind::Released, Some(_)) => {
            confirm_selection();
        }
        (Key::Back, ButtonEventKind::Pressed, _) => {
            // React immediately; a press event always has Some(0) held_ms.
            move_to_previous_item();
        }
        _ => {}
    }
}
```

### Check one key or all held keys

`is_pressed` and `held_ms` are useful for controls whose action continues
while they are held. Indexes outside the matrix are safe: they return `false`
or `None`.

```rust,ignore
// Give the Up key an auto-repeat action after 300 ms.
if matrix.is_pressed(0, 0) {
    if let Some(held_ms) = matrix.held_ms(0, 0, now_ms) {
        if held_ms >= 300 {
            move_cursor_up();
        }
    }
}

// Or update every held key. The iterator is row-major and allocates nothing.
for key in matrix.pressed_keys(now_ms) {
    match key.name {
        Key::Left => move_left(),
        Key::Right => move_right(),
        _ => {}
    }
}
```

### Active-high matrix

For pull-down rows, initialize unselected columns low and use
`MatrixConfig::active_high()`. While scanning, the selected column is driven
high and a pressed key makes its row read high.

```rust,ignore
use debounce_button_matrix_eng::{ButtonMatrix, MatrixConfig};

let rows = [row0, row1];
let columns = [col0, col1]; // Configure outputs initially low.
let names = [[Key::Up, Key::Down], [Key::Left, Key::Right]];

let config = MatrixConfig::active_high().with_debounce_ms(10);
let mut matrix = ButtonMatrix::with_config(rows, columns, names, config);
```

If the board requires mixed polarity, set the two parts separately:

```rust,ignore
use debounce_button_matrix_eng::{ActiveLevel, ColumnDrive, MatrixConfig};

let config = MatrixConfig::active_low()
    .with_row_active_level(ActiveLevel::High)
    .with_column_drive(ColumnDrive::ActiveLow);
```

### Report GPIO failures with their matrix position

`scan` returns `MatrixError`, which identifies the failing row or column.
This lets firmware add useful diagnostics without giving up the underlying GPIO
error.

```rust,ignore
use debounce_button_matrix_eng::MatrixError;

match matrix.scan(now_ms) {
    Ok(events) => handle_events(events),
    Err(MatrixError::Row { row, column, source }) => {
        log_row_error(row, column, source);
    }
    Err(MatrixError::Column { column, source }) => {
        log_column_error(column, source);
    }
}
```

## API Summary

`MatrixConfig`:

- `MatrixConfig::active_low()` - selected columns are low, pressed rows read
  low;
- `MatrixConfig::active_high()` - selected columns are high, pressed rows read
  high;
- `with_debounce_ms(...)` - debounce time in milliseconds;
- `with_row_active_level(...)` - override row input polarity;
- `with_column_drive(...)` - override column output polarity.

`ButtonMatrix`:

- `ButtonMatrix::new(rows, columns, names)`;
- `ButtonMatrix::with_config(rows, columns, names, config)`;
- `scan(now_ms)` - scan the whole matrix once;
- `state(row, column)` - current stable state for one key;
- `is_pressed(row, column)` - stable pressed state check;
- `held_ms(row, column, now_ms)` - current hold duration for one key;
- `pressed_keys(now_ms)` - iterator over all currently pressed keys;
- `rows_mut()` and `columns_mut()` - mutable access to GPIO arrays;
- `into_parts()` - return rows, columns, and names.

`ButtonMatrixEvents`:

- `count()` - number of events in the scan;
- `is_empty()` - true when no key changed;
- `event_at(row, column)` - event for one key if it changed;
- `iter()` - row-major iterator over all events.

## Timing

`scan(now_ms)` accepts monotonic time in milliseconds. The timestamp must not go
backwards. The crate uses timestamp differences to:

- decide when a raw key state passed debounce;
- compute current hold time;
- compute final hold time on release.

The scanner does not block and does not delay internally. Call it regularly from
the main loop, an RTOS task, or a timer.

## Tests

Run tests:

```powershell
cargo test
```

Run formatting and clippy checks:

```powershell
cargo fmt --check
cargo clippy --all-targets -- -D warnings
```
