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}