1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
// Copyright 2021 Jacob Alexander
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

// ----- Modules -----

#![no_std]

mod rawlookup;
mod test;

// ----- Crates -----

#[cfg(any(
    feature = "defmt-default",
    feature = "defmt-trace",
    feature = "defmt-debug",
    feature = "defmt-info",
    feature = "defmt-warn",
    feature = "defmt-error"
))]
use defmt::*;
use heapless::Vec;
#[cfg(not(any(
    feature = "defmt-default",
    feature = "defmt-trace",
    feature = "defmt-debug",
    feature = "defmt-info",
    feature = "defmt-warn",
    feature = "defmt-error"
)))]
use log::*;

// TODO Use features to determine which lookup table to use
use rawlookup::MODEL;

// ----- Sense Data -----

/// Calibration status indicates if a sensor position is ready to send
/// analysis for a particular key.
#[repr(C)]
#[derive(Clone, Debug, PartialEq, defmt::Format)]
pub enum CalibrationStatus {
    NotReady = 0,                 // Still trying to determine status (from power-on)
    SensorMissing = 1,            // ADC value at 0
    SensorBroken = 2, // Reading higher than ADC supports (invalid), or magnet is too strong
    MagnetDetected = 3, // Magnet detected, min calibrated, positive range
    MagnetWrongPoleOrMissing = 4, // Magnet detected, wrong pole direction
    InvalidIndex = 5, // Invalid index
}

#[derive(Clone, Debug, defmt::Format)]
pub enum SensorError {
    CalibrationError(SenseData),
    FailedToResize(usize),
    InvalidSensor(usize),
}

/// Calculations:
///  d = linearized(adc sample) --> distance
///  v = (d - d_prev) / 1       --> velocity
///  a = (v - v_prev) / 2       --> acceleration
///  j = (a - a_prev) / 3       --> jerk
///
/// These calculations assume constant time delta of 1
#[repr(C)]
#[derive(Clone, Debug, defmt::Format)]
pub struct SenseAnalysis {
    raw: u16,          // Raw ADC reading
    distance: i16,     // Distance value (lookup + min/max alignment)
    velocity: i16,     // Velocity calculation (*)
    acceleration: i16, // Acceleration calculation (*)
    jerk: i16,         // Jerk calculation (*)
}

impl SenseAnalysis {
    /// Using the raw value do calculations
    /// Requires the previous analysis
    pub fn new(raw: u16, data: &SenseData) -> SenseAnalysis {
        // Do raw lookup (we've already checked the bounds)
        let initial_distance = MODEL[raw as usize];

        /*
        // Min/max adjustment
        let distance_offset = match data.cal {
            CalibrationStatus::MagnetDetected => {
                // Subtract the min lookup
                // Lookup table has negative values for unexpectedly
                // small values (greater than sensor center)
                MODEL[data.stats.min as usize]
            }
            _ => {
                // Invalid reading
                return SenseAnalysis::null();
            }
        };
        */
        let distance_offset = MODEL[data.stats.min as usize];
        let distance = initial_distance - distance_offset;
        let velocity = distance - data.analysis.distance; // / 1
        let acceleration = (velocity - data.analysis.velocity) / 2;
        // NOTE: To use jerk, the compile-time thresholds will need to be
        //       multiplied by 3 (to account for the missing / 3)
        let jerk = acceleration - data.analysis.acceleration;
        SenseAnalysis {
            raw,
            distance,
            velocity,
            acceleration,
            jerk,
        }
    }

    /// Null entry
    pub fn null() -> SenseAnalysis {
        SenseAnalysis {
            raw: 0,
            distance: 0,
            velocity: 0,
            acceleration: 0,
            jerk: 0,
        }
    }
}

/// Stores incoming raw samples
#[repr(C)]
#[derive(Clone, Debug, defmt::Format)]
pub struct RawData {
    scratch_samples: u8,
    scratch: u32,
    prev_scratch: u32,
}

impl RawData {
    fn new() -> RawData {
        RawData {
            scratch_samples: 0,
            scratch: 0,
            prev_scratch: 0,
        }
    }

    /// Adds to the internal scratch location
    /// Designed to accumulate until a set number of readings added
    /// SC: specifies the number of scratch samples until ready to average
    ///     Should be a power of two (1, 2, 4, 8, 16...) for the compiler to
    ///     optimize.
    fn add<const SC: usize>(&mut self, reading: u16) -> Option<u16> {
        self.scratch += reading as u32;
        self.scratch_samples += 1;
        trace!(
            "Reading: {}  Sample: {}/{}",
            reading,
            self.scratch_samples,
            SC as u8
        );

        if self.scratch_samples == SC as u8 {
            let val = if self.prev_scratch == 0 {
                self.scratch / SC as u32
            } else {
                // Average previous value if non-zero
                (self.scratch + self.prev_scratch) / SC as u32 / SC as u32
            };
            self.prev_scratch = self.scratch;
            self.scratch = 0;
            self.scratch_samples = 0;
            Some(val as u16)
        } else {
            None
        }
    }

    /// Reset data, used when transitioning between calibration and normal modes
    fn reset(&mut self) {
        self.scratch = 0;
        self.scratch_samples = 0;
        self.prev_scratch = 0;
    }
}

/// Sense stats include statistically information about the sensor data
#[repr(C)]
#[derive(Clone, Debug, defmt::Format)]
pub struct SenseStats {
    pub min: u16,     // Minimum raw value (reset when out of calibration)
    pub max: u16,     // Maximum raw value (reset when out of calibration)
    pub samples: u32, // Total number of samples (does not reset)
}

impl SenseStats {
    fn new() -> SenseStats {
        SenseStats {
            min: 0xFFFF,
            max: 0x0000,
            samples: 0,
        }
    }

    /// Reset, resettable stats (e.g. min, max, but not samples)
    fn reset(&mut self) {
        self.min = 0xFFFF;
        self.max = 0x0000;
    }
}

/// Sense data is store per ADC source element (e.g. per key)
/// The analysis is stored in a queue, where old values expire out
/// min/max is used to handle offsets from the distance lookups
/// Higher order calculations assume a constant unit of time between measurements
/// Any division is left to compile-time comparisions as it's not necessary
/// to actually compute the final higher order values in order to make a decision.
/// This diagram can give a sense of how the incoming data is used.
/// The number represents the last ADC sample required to calculate the value.
///
/// ```text,ignore
///
///            4  5 ... <- Jerk (e.g. m/2^3)
///          / | /|
///         3  4  5 ... <- Acceleration (e.g. m/2^2)
///       / | /| /|
///      2  3  4  5 ... <- Velocity (e.g. m/s)
///    / | /| /| /|
///   1  2  3  4  5 ... <- Distance (e.g. m)
///  ----------------------
///   1  2  3  4  5 ... <== ADC Averaged Sample
///
/// ```
///
/// Distance     => Min/Max adjusted lookup
/// Velocity     => (d_current - d_previous) / 1 (constant time)
///                 There is 1 time unit between samples 1 and 2
/// Acceleration => (v_current - v_previous) / 2 (constant time)
///                 There are 2 time units between samples 1 and 3
/// Jerk         => (a_current - a_previous) / 3 (constant time)
///                 There are 3 time units between samples 1 and 4
///
/// NOTE: Division is computed at compile time for jerk (/ 3)
///
/// Time is simplified to 1 unit (normally sampling will be at a constant time-rate, so this should be somewhat accurate).
///
/// A variety of thresholds are used during calibration and normal operating modes.
/// These values are generics as there's no reason to store each of the thresholds at runtime for
/// each sensor (wastes precious sram per sensor).
///
/// Calibration Mode:
/// * MNOK: Min valid calibration (Wrong magnet direction; wrong pole, less than a specific value)
/// * MXOK: Max valid calibration (Bad Sensor threshold; sensor is bad if reading is higher than this value)
/// * NS: No sensor detected (less than a specific value)
#[derive(Clone, Debug, defmt::Format)]
pub struct SenseData {
    pub analysis: SenseAnalysis,
    pub cal: CalibrationStatus,
    pub data: RawData,
    pub stats: SenseStats,
}

impl SenseData {
    pub fn new() -> SenseData {
        SenseData {
            analysis: SenseAnalysis::null(),
            cal: CalibrationStatus::NotReady,
            data: RawData::new(),
            stats: SenseStats::new(),
        }
    }

    /// Acculumate a new sensor reading
    /// Once the required number of samples is retrieved, do analysis
    /// Analysis does a few more addition, subtraction and comparisions
    /// so it's a more expensive operation.
    /// Normal mode
    fn add<const SC: usize>(
        &mut self,
        reading: u16,
    ) -> Result<Option<&SenseAnalysis>, SensorError> {
        // Add value to accumulator
        if let Some(data) = self.data.add::<SC>(reading) {
            // Check min/max values
            if data > self.stats.max {
                self.stats.max = data;
            }
            if data < self.stats.min {
                self.stats.min = data;
            }
            trace!("Reading: {}  Stats: {:?}", reading, self.stats);

            // As soon as we have enough values accumulated, set magnet as detected in normal mode
            self.cal = CalibrationStatus::MagnetDetected;

            // Calculate new analysis (requires previous results + min/max)
            self.analysis = SenseAnalysis::new(data, self);
            Ok(Some(&self.analysis))
        } else {
            Ok(None)
        }
    }

    /// Acculumate a new sensor reading
    /// Once the required number of samples is retrieved, do analysis
    /// Analysis does a few more addition, subtraction and comparisions
    /// so it's a more expensive operation.
    /// Test mode
    fn add_test<const SC: usize, const MNOK: usize, const MXOK: usize, const NS: usize>(
        &mut self,
        reading: u16,
    ) -> Result<Option<&SenseAnalysis>, SensorError> {
        // Add value to accumulator
        if let Some(data) = self.data.add::<SC>(reading) {
            // Check min/max values
            if data > self.stats.max {
                self.stats.max = data;
            }
            if data < self.stats.min {
                self.stats.min = data;
            }

            // Check calibration
            self.cal = self.check_calibration::<MNOK, MXOK, NS>(data);
            trace!(
                "Reading: {}  Cal: {:?}  Stats: {:?}",
                reading,
                self.cal,
                self.stats
            );
            match self.cal {
                CalibrationStatus::MagnetDetected => {}
                // Don't bother doing calculations if magnet+sensor isn't ready
                _ => {
                    // Reset min/max
                    self.stats.reset();
                    // Reset averaging
                    self.data.reset();
                    // Clear analysis, only set raw
                    self.analysis = SenseAnalysis::null();
                    self.analysis.raw = data;
                    return Err(SensorError::CalibrationError(self.clone()));
                }
            }

            // Calculate new analysis (requires previous results + min/max)
            self.analysis = SenseAnalysis::new(data, self);
            Ok(Some(&self.analysis))
        } else {
            Ok(None)
        }
    }

    /// Update calibration state
    /// Calibration is different depending on whether or not we've already been successfully
    /// calibrated. Gain and offset are set differently depending on whether the sensor has been
    /// calibrated. Uncalibrated sensors run at a lower gain to gather more details around voltage
    /// limits. Wherease calibrated sensors run at higher gain (and likely an offset) to maximize
    /// the voltage range of the desired sensor range.
    /// NOTE: This implementation (currently) only works for a single magnet pole of a bipolar sensor.
    fn check_calibration<const MNOK: usize, const MXOK: usize, const NS: usize>(
        &self,
        data: u16,
    ) -> CalibrationStatus {
        // Value too high, likely a bad sensor or bad soldering on the pcb
        // Magnet may also be too strong.
        if data > MXOK as u16 {
            return CalibrationStatus::SensorBroken;
        }
        // No sensor detected
        if data < NS as u16 {
            return CalibrationStatus::SensorMissing;
        }
        // Wrong pole (or magnet may be too weak)
        if data < MNOK as u16 {
            return CalibrationStatus::MagnetWrongPoleOrMissing;
        }

        CalibrationStatus::MagnetDetected
    }
}

impl Default for SenseData {
    fn default() -> Self {
        SenseData::new()
    }
}

// ----- Hall Effect Interface ------

pub struct Sensors<const S: usize> {
    sensors: Vec<SenseData, S>,
}

impl<const S: usize> Sensors<S> {
    /// Initializes full Sensor array
    /// Only fails if static allocation fails (very unlikely)
    pub fn new() -> Result<Sensors<S>, SensorError> {
        let mut sensors = Vec::new();
        if sensors.resize_default(S).is_err() {
            Err(SensorError::FailedToResize(S))
        } else {
            Ok(Sensors { sensors })
        }
    }

    /// Add sense data for a specific sensor
    pub fn add<const SC: usize>(
        &mut self,
        index: usize,
        reading: u16,
    ) -> Result<Option<&SenseAnalysis>, SensorError> {
        trace!("Index: {}  Reading: {}", index, reading);
        if index < self.sensors.len() {
            self.sensors[index].add::<SC>(reading)
        } else {
            Err(SensorError::InvalidSensor(index))
        }
    }

    /// Add sense data for a specific sensor
    /// Test mode
    pub fn add_test<const SC: usize, const MNOK: usize, const MXOK: usize, const NS: usize>(
        &mut self,
        index: usize,
        reading: u16,
    ) -> Result<Option<&SenseAnalysis>, SensorError> {
        trace!("Index: {}  Reading: {}", index, reading);
        if index < self.sensors.len() {
            self.sensors[index].add_test::<SC, MNOK, MXOK, NS>(reading)
        } else {
            Err(SensorError::InvalidSensor(index))
        }
    }

    pub fn get_data(&self, index: usize) -> Result<&SenseData, SensorError> {
        if index < self.sensors.len() {
            if self.sensors[index].cal == CalibrationStatus::NotReady {
                Err(SensorError::CalibrationError(self.sensors[index].clone()))
            } else {
                Ok(&self.sensors[index])
            }
        } else {
            Err(SensorError::InvalidSensor(index))
        }
    }
}