rotary_encoder_embedded/
quadrature.rs

1use embedded_hal::digital::InputPin;
2
3use crate::{Direction, RotaryEncoder};
4
5/// Quadrature Lookup Table
6/// Index = (prev_state << 2) | curr_state
7/// Value = +1 for CW, -1 for CCW, 0 for no movement/invalid
8const QUAD_TABLE: [i8; 16] = [
9    0,  // 00 -> 00
10    1,  // 00 -> 01  = CW
11    -1, // 00 -> 10  = CCW
12    0,  // 00 -> 11  = invalid (skipped state)
13    -1, // 01 -> 00  = CCW
14    0,  // 01 -> 01
15    0,  // 01 -> 10  = invalid
16    1,  // 01 -> 11  = CW
17    1,  // 10 -> 00  = CW
18    0,  // 10 -> 01  = invalid
19    0,  // 10 -> 10
20    -1, // 10 -> 11  = CCW
21    0,  // 11 -> 00  = invalid
22    -1, // 11 -> 01  = CCW
23    1,  // 11 -> 10  = CW
24    0,  // 11 -> 11
25];
26
27impl<DT, CLK> RotaryEncoder<QuadratureTableMode, DT, CLK>
28where
29    DT: InputPin,
30    CLK: InputPin,
31{
32    /// Updates the `RotaryEncoder`, updating the `direction` property
33    pub fn update(&mut self) -> Direction {
34        self.mode.update(
35            self.pin_dt.is_high().unwrap_or_default(),
36            self.pin_clk.is_high().unwrap_or_default(),
37        )
38    }
39}
40
41impl<LOGIC, DT, CLK> RotaryEncoder<LOGIC, DT, CLK>
42where
43    DT: InputPin,
44    CLK: InputPin,
45{
46    /// Configure `RotaryEncoder` to use the quadrature table mode
47    pub fn into_quadrature_table_mode(
48        self,
49        threshold: u8,
50    ) -> RotaryEncoder<QuadratureTableMode, DT, CLK> {
51        RotaryEncoder {
52            pin_dt: self.pin_dt,
53            pin_clk: self.pin_clk,
54            mode: QuadratureTableMode::new(threshold),
55        }
56    }
57}
58
59impl Default for QuadratureTableMode {
60    fn default() -> Self {
61        Self::new(1)
62    }
63}
64
65/// Quadrature Table Encoder Mode
66/// This mode is suitable for indentless encoders
67pub struct QuadratureTableMode {
68    prev_state: u8, // lower two bits only
69    threshold: u8,  // how many “deltas” before we report a step
70    count: i8,      // running sum of +1/–1 deltas
71}
72
73impl QuadratureTableMode {
74    /// Initializes Quadrature table encoder
75    /// `threshold` - the number of events before a Direction is yielded. By default this value is 1 for the most sensitivity.
76    pub fn new(threshold: u8) -> Self {
77        Self {
78            prev_state: 0,
79            count: 0,
80            threshold,
81        }
82    }
83
84    /// Call this on every A/B change (or in a tight loop)
85    /// dt = data pin, clk = clock pin levels (0 or 1)
86    pub fn update(&mut self, dt: bool, clk: bool) -> Direction {
87        let curr = (dt as u8) | ((clk as u8) << 1);
88        let idx = ((self.prev_state << 2) | curr) as usize;
89        let delta = QUAD_TABLE[idx];
90        self.prev_state = curr;
91        self.count += delta;
92        if self.count.unsigned_abs() >= self.threshold {
93            let dir = if self.count > 0 {
94                Direction::Clockwise
95            } else {
96                Direction::Anticlockwise
97            };
98            self.count = 0; // reset and only report once
99            return dir;
100        }
101        Direction::None
102    }
103}
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::Direction;
108
109    /// Test‑only helper; collects the result of each update.
110    fn drive_sequence(mode: &mut QuadratureTableMode, seq: &[(bool, bool)]) -> Vec<Direction> {
111        seq.iter().map(|&(dt, clk)| mode.update(dt, clk)).collect()
112    }
113
114    #[test]
115    fn single_cw_step_threshold_1() {
116        let mut mode = QuadratureTableMode::new(1);
117        // 00 -> 01 (+1) => immediately CW
118        assert_eq!(mode.update(true, false), Direction::Clockwise);
119    }
120
121    #[test]
122    fn single_ccw_step_threshold_1() {
123        let mut mode = QuadratureTableMode::new(1);
124        // 00 -> 10 (–1) => immediately CCW
125        assert_eq!(mode.update(false, true), Direction::Anticlockwise);
126    }
127
128    #[test]
129    fn aggregation_threshold_2_requires_two_valid_pulses() {
130        // threshold = 2: need two *valid* deltas before firing
131        let mut mode = QuadratureTableMode::new(2);
132
133        // First valid CW pulse: 00->01 = +1, count=1 < 2 => None
134        assert_eq!(mode.update(true, false), Direction::None);
135
136        // Next valid CW pulse: 01->11 = +1, count=2 >= 2 => CW
137        assert_eq!(mode.update(true, true), Direction::Clockwise);
138
139        // Counter reset: another pulse 11->10 = +1 gives None
140        assert_eq!(mode.update(false, true), Direction::None);
141    }
142
143    #[test]
144    fn no_movement_on_constant_state() {
145        let mut mode = QuadratureTableMode::new(1);
146        // Stay in 00 the whole time
147        for _ in 0..5 {
148            assert_eq!(mode.update(false, false), Direction::None);
149        }
150    }
151
152    #[test]
153    fn invalid_transition_skipped_state() {
154        let mut mode = QuadratureTableMode::new(1);
155        // 00 -> 11 is invalid (table[0b0011] == 0)
156        assert_eq!(mode.update(true, true), Direction::None);
157        // And back 11 -> 00 is also table[0b1100] == 0
158        assert_eq!(mode.update(false, false), Direction::None);
159    }
160
161    #[test]
162    fn full_cw_cycle_threshold_1() {
163        let mut mode = QuadratureTableMode::new(1);
164        // A full 4‑step CW quadrature cycle: 00→01→11→10→00
165        let seq = [
166            (false, false), // 00→00 = 0
167            (true, false),  // 00→01 = +1 → CW
168            (true, true),   // 01→11 = +1 → CW
169            (false, true),  // 11→10 = +1 → CW
170            (false, false), // 10→00 = +1 → CW
171        ];
172        let results = drive_sequence(&mut mode, &seq);
173        assert_eq!(
174            results,
175            vec![
176                Direction::None,
177                Direction::Clockwise,
178                Direction::Clockwise,
179                Direction::Clockwise,
180                Direction::Clockwise,
181            ]
182        );
183    }
184}