keebrs 0.3.0

Keyboard firmware building blocks
//! Stateful keyboard scanning implementation

use core::{
    num::NonZeroUsize,
    pin::Pin,
    task::{
        Context,
        Poll,
    },
};

use alloc::vec::Vec;

use crate::{
    debounce::{
        DebounceState,
        Debouncer,
        State,
    },
    key::*,
    matrix::Scan,
    stateful::ScanStateful,
};

/// Stateful keyboard matrix scanner
pub struct StatefulScanner<Sc, St, D> {
    debouncer: Debouncer<Sc, St, D>,

    per_group: NonZeroUsize,
    num_groups: NonZeroUsize,
    cur_group: usize,
}

/// Convenience alias for a `Vec`-backed scanner
pub type VecScanner<Sc, D> = StatefulScanner<Sc, Vec<Vec<DebounceState>>, D>;

impl<Sc, St, D> StatefulScanner<Sc, St, D>
where
    Sc: Scan,
    St: State<Sc>,
    D: FnMut(),
{
    /// Create a new [StatefulScanner]
    ///
    /// Currently only supports "quick" debouncing with the given threshold.
    pub fn new(scanner: Sc, debounce: usize, delay: D) -> Self {
        Self::with_groups(scanner, debounce, 1, delay)
    }

    /// Create a new [StatefulScanner]
    ///
    /// `groups` > 1 will cause the scanner to divide scanning into a number of
    /// line groups and will yield between groups. This will allow other tasks to
    /// run without having to wait for a full scan.
    pub fn with_groups(scanner: Sc, debounce: usize, groups: usize, delay: D) -> Self {
        StatefulScanner {
            debouncer: Debouncer::new(scanner, true, debounce, delay),
            per_group: NonZeroUsize::new(Sc::NPULL / groups)
                .expect("cannot have more groups than lines to pull"),
            num_groups: NonZeroUsize::new(groups).expect("cannot have zero scan groups"),
            cur_group: 0,
        }
    }
}

impl<Sc, St, D> ScanStateful for StatefulScanner<Sc, St, D>
where
    Sc: Scan + Unpin,
    St: State<Sc> + Unpin,
    D: FnMut() + Unpin,
{
    fn poll_change(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<PhysKey>> {
        let this = self.get_mut();

        let offset = this.cur_group * this.per_group.get();

        // The final group may have more lines if they don't fit evenly into the
        // groups.
        let end = if (this.cur_group + 1) == this.num_groups.get() {
            Sc::NPULL
        } else {
            offset + this.per_group.get()
        };

        let debouncer = &mut this.debouncer;

        for x in offset..end {
            if let Some((y, state)) = debouncer.debounce_x(x) {
                return Poll::Ready(Some(PhysKey {
                    pos: KeyPos {
                        x: x as u8,
                        y: y as u8,
                    },
                    state,
                }));
            }
        }

        // Only update the current group after we've decided that there weren't
        // any changes in this one. This way, the current group will be
        // re-scanned to be a bit more fair
        this.cur_group += 1;

        if this.cur_group == this.num_groups.get() {
            this.cur_group = 0;
            return Poll::Ready(None);
        }

        cx.waker().wake_by_ref();
        Poll::Pending
    }
}