kiibohd_hall_effect/
lib.rs

1// Copyright 2021-2023 Jacob Alexander
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8// ----- Modules -----
9
10#![no_std]
11
12//mod test; // TODO
13pub mod lookup;
14
15// ----- Crates -----
16
17#[cfg(feature = "defmt")]
18use defmt::*;
19use heapless::Vec;
20#[cfg(not(feature = "defmt"))]
21use log::*;
22
23// ----- Sense Data -----
24
25/// Indicates mode of the sensor
26/// Used to specify a different lookup table and data processing behaviour
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28#[cfg_attr(feature = "defmt", derive(defmt::Format))]
29pub enum SensorMode {
30    /// Run ADC in reduced precision mode to collect additional calibration data
31    /// Generally uses an alternate lookup table
32    Test(&'static lookup::Entry),
33    /// Low latency mode (usually the same as NormalMode)
34    LowLatency(&'static lookup::Entry),
35    /// Normal mode for ADC
36    Normal(&'static lookup::Entry),
37}
38
39impl SensorMode {
40    pub fn entry(&self) -> &lookup::Entry {
41        match self {
42            SensorMode::Test(entry) => entry,
43            SensorMode::LowLatency(entry) => entry,
44            SensorMode::Normal(entry) => entry,
45        }
46    }
47}
48
49/// Calibration status indicates if a sensor position is ready to send
50/// analysis for a particular key.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52#[cfg_attr(feature = "defmt", derive(defmt::Format))]
53pub enum CalibrationStatus {
54    /// Still trying to determine status (from power-on)
55    /// Raw value must settle within a range before the sensor is considered ready
56    /// This range is determined by the ADC mode that is currently set
57    /// (e.g. 600-800 for at least 5 seconds)
58    NotReady = 0,
59    /// ADC value at 0 (test mode only)
60    SensorMissing = 1,
61    /// Reading higher than ADC supports (invalid), or magnet is too strong (test mode only)
62    SensorBroken = 2,
63    /// Sensor value low (not enough data to quantify further)
64    SensorLow = 6,
65    /// Magnet detected, min calibrated, positive range
66    MagnetDetected = 3,
67    /// Magnet detected, wrong pole direction (test mode only)
68    MagnetWrongPole = 4,
69    /// Invalid index
70    InvalidIndex = 5,
71}
72
73impl CalibrationStatus {
74    /// Update calibration status
75    /// Returns true if the sensor is ready/calibrated
76    pub fn update_calibration(&mut self, reading: u16, mode: SensorMode) -> bool {
77        let entry = mode.entry();
78        // Make sure reading isn't too low
79        if reading < entry.min_ok_value {
80            // Don't try to determine the true state, we'll do that later
81            *self = CalibrationStatus::NotReady;
82        }
83        self.is_calibrated()
84    }
85
86    /// Easy check whether or not the sensor is ready
87    pub fn is_calibrated(&self) -> bool {
88        matches!(self, CalibrationStatus::MagnetDetected)
89    }
90
91    /// Detailed calibration status
92    /// Returns a more detailed calibration status (takes a few more steps and is not necessary
93    /// during normal operation)
94    pub fn detailed_calibration(&self, data: &SenseData) -> CalibrationStatus {
95        match self {
96            CalibrationStatus::MagnetDetected => *self,
97            _ => {
98                match data.mode {
99                    // More detailed analysis due to additional ADC range
100                    SensorMode::Test(entry) => {
101                        if data.data.value == 0 {
102                            CalibrationStatus::SensorMissing
103                        } else if data.data.value > entry.max_ok_value {
104                            CalibrationStatus::SensorBroken
105                        } else if data.data.value < entry.min_ok_value {
106                            CalibrationStatus::MagnetWrongPole
107                        } else if data.data.value < entry.min_idle_value {
108                            CalibrationStatus::SensorLow
109                        } else {
110                            CalibrationStatus::NotReady
111                        }
112                    }
113                    // Simplified analysis due to optimized ADC range
114                    SensorMode::LowLatency(entry) | SensorMode::Normal(entry) => {
115                        if data.data.value < entry.min_idle_value {
116                            CalibrationStatus::SensorLow
117                        } else {
118                            CalibrationStatus::NotReady
119                        }
120                    }
121                }
122            }
123        }
124    }
125}
126
127#[derive(Clone, Debug)]
128#[cfg_attr(feature = "defmt", derive(defmt::Format))]
129pub enum SensorError {
130    CalibrationError(SenseData),
131    FailedToResize(usize),
132    InvalidSensor(usize),
133}
134
135/// Records momentary push button events
136///
137/// Cycles can be converted to time by multiplying by the scan period (Matrix::period())
138#[derive(Copy, Clone, Debug, PartialEq, Eq)]
139#[cfg_attr(feature = "defmt", derive(defmt::Format))]
140pub enum KeyState {
141    /// Passed activation point
142    On {
143        /// Cycles since the last state change
144        cycles_since_state_change: u32,
145    },
146    /// Passed deactivation point
147    Off {
148        /// Cycles since the last state change
149        cycles_since_state_change: u32,
150    },
151}
152
153/// Calculations:
154///  d = linearized(adc sample) --> distance
155///  v = (d - d_prev) / 1       --> velocity
156///  a = (v - v_prev) / 2       --> acceleration
157///  j = (a - a_prev) / 3       --> jerk
158///
159/// These calculations assume constant time delta of 1
160#[derive(Clone, Debug)]
161#[cfg_attr(feature = "defmt", derive(defmt::Format))]
162pub struct SenseAnalysis {
163    /// Threshold state
164    pub state: KeyState,
165    /// Distance value (lookup + min/max alignment)
166    pub distance: i16,
167    /// Velocity calculation (*)
168    pub velocity: i16,
169    /// Acceleration calculation (*)
170    pub acceleration: i16,
171    /// Jerk calculation (*)
172    pub jerk: i16,
173}
174
175impl SenseAnalysis {
176    /// Using the raw value do calculations
177    pub fn new(data: &SenseData) -> Self {
178        // Lookup distance
179        let entry = data.mode.entry();
180        let distance = entry.lookup(data.data.value, data.raw_offset);
181
182        // Update key state
183        let state = match data.analysis.state {
184            KeyState::On {
185                cycles_since_state_change,
186            } => {
187                if distance <= data.deactivation {
188                    // Key is now off
189                    KeyState::Off {
190                        cycles_since_state_change: 0,
191                    }
192                } else {
193                    // Key is still on
194                    KeyState::On {
195                        cycles_since_state_change: cycles_since_state_change.saturating_add(1),
196                    }
197                }
198            }
199            KeyState::Off {
200                cycles_since_state_change,
201            } => {
202                if distance >= data.activation {
203                    // Key is now on
204                    KeyState::On {
205                        cycles_since_state_change: 0,
206                    }
207                } else {
208                    // Key is still off
209                    KeyState::Off {
210                        cycles_since_state_change: cycles_since_state_change.saturating_add(1),
211                    }
212                }
213            }
214        };
215
216        // Calculate velocity/acceleration/jerk
217        let velocity = distance - data.analysis.distance; // / 1
218        let acceleration = (velocity - data.analysis.velocity) / 2;
219        // NOTE: To use jerk, the compile-time thresholds will need to be
220        //       multiplied by 3 (to account for the missing / 3)
221        let jerk = acceleration - data.analysis.acceleration;
222        SenseAnalysis {
223            state,
224            distance,
225            velocity,
226            acceleration,
227            jerk,
228        }
229    }
230
231    /// Null entry
232    pub fn null() -> SenseAnalysis {
233        SenseAnalysis {
234            state: KeyState::Off {
235                cycles_since_state_change: u32::MAX,
236            },
237            distance: 0,
238            velocity: 0,
239            acceleration: 0,
240            jerk: 0,
241        }
242    }
243}
244
245/// Stores incoming raw samples
246#[derive(Clone, Debug)]
247#[cfg_attr(feature = "defmt", derive(defmt::Format))]
248pub struct RawData {
249    value: u16,
250    average: u16,
251    idle_count: u32,
252}
253
254impl RawData {
255    /// Create a RawData instance
256    pub fn new() -> Self {
257        Self {
258            value: 0,
259            average: 0,
260            idle_count: 0,
261        }
262    }
263
264    /// Updates the raw value with a new reading.
265    /// - Updates the running average
266    /// - Increments the idle if average is within specified range
267    /// - If idle count exceeds the specified threshold, the average is returned
268    ///   This average is used to calibrate the minimum distance
269    pub fn update<const IDLE_LIMIT: usize>(&mut self, value: u16, mode: SensorMode) -> Option<u16> {
270        self.value = value;
271        // Update average
272        self.average = (self.average + value) / 2;
273
274        // Update idle count
275        let entry = mode.entry();
276        if self.average >= entry.min_idle_value && self.average <= entry.max_idle_value {
277            self.idle_count += 1;
278        } else {
279            self.idle_count = 0;
280        }
281        trace!(
282            "RawData::update: value: {}, average: {}, idle_count: {} ({}..{}:{})",
283            self.value,
284            self.average,
285            self.idle_count,
286            entry.min_idle_value,
287            entry.max_idle_value,
288            entry.sensor_zero,
289        );
290
291        // Return average if idle count exceeds threshold
292        if self.idle_count > IDLE_LIMIT as u32 {
293            Some(self.average)
294        } else {
295            None
296        }
297    }
298
299    /// Returns the current value
300    pub fn value(&self) -> u16 {
301        self.value
302    }
303
304    /// Returns the current average
305    pub fn average(&self) -> u16 {
306        self.average
307    }
308
309    /// Reset data, used when transitioning between calibration and normal modes
310    pub fn reset(&mut self) {
311        self.value = 0;
312        self.average = 0;
313        self.idle_count = 0;
314    }
315}
316
317impl Default for RawData {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323/// Sense data is store per ADC source element (e.g. per key)
324/// The analysis is stored in a queue, where old values expire out
325/// min/max is used to handle offsets from the distance lookups
326/// Higher order calculations assume a constant unit of time between measurements
327/// Any division is left to compile-time comparisions as it's not necessary
328/// to actually compute the final higher order values in order to make a decision.
329/// This diagram can give a sense of how the incoming data is used.
330/// The number represents the last ADC sample required to calculate the value.
331///
332/// ```text,ignore
333///
334///            4  5 ... <- Jerk (e.g. m/2^3)
335///          / | /|
336///         3  4  5 ... <- Acceleration (e.g. m/2^2)
337///       / | /| /|
338///      2  3  4  5 ... <- Velocity (e.g. m/s)
339///    / | /| /| /|
340///   1  2  3  4  5 ... <- Distance (e.g. m)
341///  ----------------------
342///   1  2  3  4  5 ... <== ADC Averaged Sample
343///
344/// ```
345///
346/// Distance     => Min/Max adjusted lookup
347/// Velocity     => (d_current - d_previous) / 1 (constant time)
348///                 There is 1 time unit between samples 1 and 2
349/// Acceleration => (v_current - v_previous) / 2 (constant time)
350///                 There are 2 time units between samples 1 and 3
351/// Jerk         => (a_current - a_previous) / 3 (constant time)
352///                 There are 3 time units between samples 1 and 4
353///
354/// NOTE: Division is computed at compile time for jerk (/ 3)
355///
356/// Time is simplified to 1 unit (normally sampling will be at a constant time-rate, so this should be somewhat accurate).
357///
358/// A variety of thresholds are used during calibration and normal operating modes.
359/// These values are generics as there's no reason to store each of the thresholds at runtime for
360/// each sensor (wastes precious sram per sensor).
361#[derive(Clone, Debug)]
362#[cfg_attr(feature = "defmt", derive(defmt::Format))]
363pub struct SenseData {
364    /// Computed lookup for the raw ADC data
365    analysis: SenseAnalysis,
366    /// Calibration status of the sensor
367    cal: CalibrationStatus,
368    /// Raw data tracking of the sensor
369    data: RawData,
370    /// Temperature/humidity compensation for the ADC->distance lookup
371    raw_offset: i16,
372    /// Processing mode + lookup table for ADC
373    mode: SensorMode,
374    /// Activation point (distance, push direction)
375    /// TODO - The logic doesn't handle negative distance values yet where activation is less than
376    /// deactivation.
377    activation: i16,
378    /// Deactivation point (distance, release direction)
379    deactivation: i16,
380}
381
382impl SenseData {
383    /// Create a new SenseData instance
384    /// - mode: Sensor mode
385    /// - activation: Activation point (distance, push direction)
386    /// - deactivation: Deactivation point (distance, release direction)
387    pub fn new(mode: SensorMode, activation: i16, deactivation: i16) -> SenseData {
388        SenseData {
389            analysis: SenseAnalysis::null(),
390            cal: CalibrationStatus::NotReady,
391            data: RawData::new(),
392            raw_offset: 0, // Starts in NotReady mode, so this value is ignored
393            mode,
394            activation,
395            deactivation,
396        }
397    }
398
399    /// Add new sensor reading
400    /// Only returns a value once the sensor has been properly calibrated and is within
401    /// the expected range.
402    pub fn add<const IDLE_LIMIT: usize>(&mut self, reading: u16) -> Option<&SenseData> {
403        // Update raw data
404        if let Some(average) = self.data.update::<IDLE_LIMIT>(reading, self.mode) {
405            // New minimum value detected
406            // Due to temperature and humidity, the sensor may drift
407            //
408            // Calculate the offset from the pre-computed lookup table
409            let entry = self.mode.entry();
410            self.raw_offset = average as i16 - entry.sensor_zero as i16;
411
412            // When we have a new valid minimum value calibration is complete
413            self.cal = CalibrationStatus::MagnetDetected;
414        }
415
416        // If sensor is calibrated compute SenseAnalysis
417        // Make sure the incoming value doesn't break the calibration
418        if self.cal.update_calibration(reading, self.mode) {
419            // Update analysis
420            self.analysis = SenseAnalysis::new(self);
421            Some(self)
422        } else {
423            // If key state was active before, deactivate it and send an event
424            match self.analysis.state {
425                KeyState::On { .. } => {
426                    self.analysis = SenseAnalysis::null();
427                    Some(self)
428                }
429                _ => None,
430            }
431        }
432    }
433
434    /// Current sensor analysis
435    pub fn analysis(&self) -> Option<&SenseAnalysis> {
436        if self.cal.is_calibrated() {
437            Some(&self.analysis)
438        } else {
439            None
440        }
441    }
442
443    /// Current sensor calibration status
444    pub fn calibration(&self) -> CalibrationStatus {
445        self.cal
446    }
447
448    /// Current raw sensor data
449    pub fn data(&self) -> &RawData {
450        &self.data
451    }
452
453    /// Raw offset value
454    pub fn raw_offset(&self) -> i16 {
455        self.raw_offset
456    }
457
458    /// Current sensor mode
459    pub fn mode(&self) -> SensorMode {
460        self.mode
461    }
462
463    /// Current activation point
464    pub fn activation(&self) -> i16 {
465        self.activation
466    }
467
468    /// Current deactivation point
469    pub fn deactivation(&self) -> i16 {
470        self.deactivation
471    }
472
473    /// Update activation/deactivation points
474    pub fn update_activation(&mut self, activation: i16, deactivation: i16) {
475        self.activation = activation;
476        self.deactivation = deactivation;
477    }
478
479    /// Change sensor mode
480    pub fn update_mode(&mut self, mode: SensorMode) {
481        self.mode = mode;
482    }
483}
484
485// ----- Hall Effect Interface ------
486
487pub struct Sensors<const S: usize> {
488    sensors: Vec<SenseData, S>,
489}
490
491impl<const S: usize> Sensors<S> {
492    /// Initializes full Sensor array
493    /// Only fails if static allocation fails (very unlikely)
494    /// - mode: Sensor mode
495    /// - activation: Activation point (distance, push direction)
496    /// - deactivation: Deactivation point (distance, release direction)
497    pub fn new(
498        mode: SensorMode,
499        activation: i16,
500        deactivation: i16,
501    ) -> Result<Sensors<S>, SensorError> {
502        let mut sensors = Vec::new();
503        if sensors
504            .resize(S, SenseData::new(mode, activation, deactivation))
505            .is_err()
506        {
507            Err(SensorError::FailedToResize(S))
508        } else {
509            Ok(Sensors { sensors })
510        }
511    }
512
513    /// Add sense data for a specific sensor
514    pub fn add<const IDLE_LIMIT: usize>(
515        &mut self,
516        index: usize,
517        reading: u16,
518    ) -> Result<Option<&SenseData>, SensorError> {
519        trace!("Index: {}  Reading: {}", index, reading);
520        if index < self.sensors.len() {
521            Ok(self.sensors[index].add::<IDLE_LIMIT>(reading))
522        } else {
523            Err(SensorError::InvalidSensor(index))
524        }
525    }
526
527    pub fn get_data(&self, index: usize) -> Result<&SenseData, SensorError> {
528        if index < self.sensors.len() {
529            if self.sensors[index].cal == CalibrationStatus::NotReady {
530                Err(SensorError::CalibrationError(self.sensors[index].clone()))
531            } else {
532                Ok(&self.sensors[index])
533            }
534        } else {
535            Err(SensorError::InvalidSensor(index))
536        }
537    }
538
539    /// Max number of sensors
540    pub fn len(&self) -> usize {
541        S
542    }
543
544    pub fn is_empty(&self) -> bool {
545        S == 0
546    }
547}
548
549#[cfg(feature = "kll-core")]
550mod converters {
551    #[cfg(feature = "defmt")]
552    use defmt::*;
553    #[cfg(not(feature = "defmt"))]
554    use log::*;
555
556    use crate::{CalibrationStatus, KeyState, SenseData, SensorMode};
557    use heapless::Vec;
558    use kll_core::layout::TriggerEventIterator;
559
560    impl SenseData {
561        /// Convert SenseData to a TriggerEvent
562        /// Criteria used to generate the event (an event may not be ready yet)
563        /// - Distance movement must be non-zero (velocity)
564        /// - Enough samples must be generated for each kind of event
565        ///   This only matters when initializing the datastructures, steady-state always has
566        ///   enough samples
567        ///   * 1 sample for distance
568        ///   * 2 samples for velocity
569        ///   * 3 samples for acceleration
570        ///   * 4 samples for jerk
571        ///
572        /// In LowLatency mode only PressHoldReleaseOff events are generated using the per key
573        /// activation point configuration
574        pub fn trigger_events<const MAX_EVENTS: usize>(
575            &self,
576            index: usize,
577            ignore_off: bool,
578        ) -> TriggerEventIterator<MAX_EVENTS> {
579            let mut events = Vec::new();
580
581            // Only create events if the sensor is calibrated
582            if self.cal == CalibrationStatus::MagnetDetected {
583                // Handle on/off events
584                match self.analysis.state {
585                    KeyState::On {
586                        cycles_since_state_change,
587                    } => {
588                        if cycles_since_state_change == 0 {
589                            trace!("Reading: {} {:?}", index, self.analysis.state);
590                            events
591                                .push(kll_core::TriggerEvent::Switch {
592                                    state: kll_core::trigger::Phro::Press,
593                                    index: index as u16,
594                                    last_state: 0,
595                                })
596                                .unwrap();
597                        } else {
598                            events
599                                .push(kll_core::TriggerEvent::Switch {
600                                    state: kll_core::trigger::Phro::Hold,
601                                    index: index as u16,
602                                    last_state: cycles_since_state_change,
603                                })
604                                .unwrap();
605                        }
606                    }
607                    KeyState::Off {
608                        cycles_since_state_change,
609                    } => {
610                        if cycles_since_state_change == 0 {
611                            trace!("Reading: {} {:?}", index, self.analysis.state);
612                            events
613                                .push(kll_core::TriggerEvent::Switch {
614                                    state: kll_core::trigger::Phro::Release,
615                                    index: index as u16,
616                                    last_state: 0,
617                                })
618                                .unwrap();
619                        // Ignore off events unless ignore_off is set
620                        } else if !ignore_off {
621                            events
622                                .push(kll_core::TriggerEvent::Switch {
623                                    state: kll_core::trigger::Phro::Off,
624                                    index: index as u16,
625                                    last_state: cycles_since_state_change,
626                                })
627                                .unwrap();
628                        }
629                    }
630                }
631
632                // Handle analog events
633                match self.mode() {
634                    SensorMode::Test(_) | SensorMode::Normal(_) => {
635                        if self.analysis.velocity != 0 || ignore_off {
636                            events
637                                .extend_from_slice(&[
638                                    kll_core::TriggerEvent::AnalogDistance {
639                                        index: index as u16,
640                                        val: self.analysis.distance,
641                                    },
642                                    kll_core::TriggerEvent::AnalogVelocity {
643                                        index: index as u16,
644                                        val: self.analysis.velocity,
645                                    },
646                                    kll_core::TriggerEvent::AnalogAcceleration {
647                                        index: index as u16,
648                                        val: self.analysis.acceleration,
649                                    },
650                                    kll_core::TriggerEvent::AnalogJerk {
651                                        index: index as u16,
652                                        val: self.analysis.jerk,
653                                    },
654                                ])
655                                .unwrap()
656                        }
657                    }
658                    _ => {}
659                }
660            }
661            TriggerEventIterator::new(events)
662        }
663    }
664}