Skip to main content

km003c_lib/
adcqueue.rs

1use num_enum::TryFromPrimitive;
2use std::fmt;
3use uom::si::electric_current::microampere;
4use uom::si::electric_potential::{microvolt, millivolt};
5use uom::si::f64::{ElectricCurrent, ElectricPotential, Frequency, Power, Time};
6use uom::si::frequency::hertz;
7use uom::si::time::millisecond;
8use zerocopy::byteorder::little_endian::{I32, U16};
9use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "python")]
15use uom::si::{electric_current::ampere, electric_potential::volt, power::watt};
16
17/// Sample rate for AdcQueue streaming mode
18///
19/// Used with StartGraph (0x0E) command to configure device sampling rate.
20/// The device expects the rate index directly (0-3).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
22#[repr(u16)]
23pub enum GraphSampleRate {
24    /// 2 samples per second
25    Sps2 = 0,
26    /// 10 samples per second
27    Sps10 = 1,
28    /// 50 samples per second
29    Sps50 = 2,
30    /// 1000 samples per second
31    Sps1000 = 3,
32}
33
34impl GraphSampleRate {
35    const SEQUENCE_TICKS_PER_SECOND: u16 = 1000;
36
37    const fn sample_count_per_second(self) -> u16 {
38        match self {
39            Self::Sps2 => 2,
40            Self::Sps10 => 10,
41            Self::Sps50 => 50,
42            Self::Sps1000 => 1000,
43        }
44    }
45
46    /// Expected increment of the device's 1000 Hz sequence counter.
47    pub const fn sequence_step(self) -> u16 {
48        Self::SEQUENCE_TICKS_PER_SECOND / self.sample_count_per_second()
49    }
50
51    /// Sampling frequency represented as a typed physical quantity.
52    pub fn frequency(self) -> Frequency {
53        Frequency::new::<hertz>(f64::from(self.sample_count_per_second()))
54    }
55
56    /// Frequency of the wrapping sequence counter used by graph samples.
57    pub fn sequence_counter_frequency() -> Frequency {
58        Frequency::new::<hertz>(f64::from(Self::SEQUENCE_TICKS_PER_SECOND))
59    }
60
61    /// Voltage represented by one auxiliary-line raw count.
62    ///
63    /// Recorded traffic and measurements against firmware 1.9.9 show that
64    /// 2 SPS uses 0.1 mV units, while faster graph modes use 1 mV units.
65    pub fn auxiliary_voltage_lsb(self) -> ElectricPotential {
66        match self {
67            Self::Sps2 => ElectricPotential::new::<millivolt>(0.1),
68            Self::Sps10 | Self::Sps50 | Self::Sps1000 => ElectricPotential::new::<millivolt>(1.0),
69        }
70    }
71
72    /// Infer the configured rate from a contiguous sequence-counter step.
73    pub const fn from_sequence_step(step: u16) -> Option<Self> {
74        match step {
75            500 => Some(Self::Sps2),
76            100 => Some(Self::Sps10),
77            20 => Some(Self::Sps50),
78            1 => Some(Self::Sps1000),
79            _ => None,
80        }
81    }
82
83    /// Number of samples missing between two sequence counter values.
84    pub fn missing_samples(self, previous: u16, current: u16) -> u16 {
85        let elapsed_ticks = current.wrapping_sub(previous);
86        elapsed_ticks.saturating_sub(self.sequence_step()) / self.sequence_step()
87    }
88}
89
90/// Elapsed device time between two values of the wrapping AdcQueue sequence counter.
91pub fn sequence_elapsed(previous: u16, current: u16) -> Time {
92    let elapsed_ticks = current.wrapping_sub(previous);
93    Time::new::<millisecond>(f64::from(elapsed_ticks))
94}
95
96impl fmt::Display for GraphSampleRate {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::Sps2 => write!(f, "2 SPS"),
100            Self::Sps10 => write!(f, "10 SPS"),
101            Self::Sps50 => write!(f, "50 SPS"),
102            Self::Sps1000 => write!(f, "1000 SPS"),
103        }
104    }
105}
106
107/// AdcQueue sample structure (20 bytes)
108///
109/// AdcQueue provides high-rate streaming of power measurements.
110/// Each sample contains voltage/current measurements for all major lines
111/// (VBUS, IBUS, CC1, CC2, D+, D-) but without statistics or temperature.
112///
113/// Typical usage: Device buffers 5-48 samples and sends them in a single
114/// USB transfer for efficient high-rate logging (up to 1000 SPS).
115///
116/// Fields NOT included (unlike ADC):
117/// - Temperature (always request ADC separately for temp)
118/// - Statistics (min/max/avg)
119/// - VDD (internal voltage)
120#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
121#[repr(C)]
122pub struct AdcQueueSampleRaw {
123    /// Incrementing sequence number for detecting dropped samples
124    pub sequence: U16,
125    /// Marker/flags field (varies by firmware version and mode, not temperature)
126    pub marker: U16,
127    /// VBUS voltage in microvolts (µV)
128    pub vbus_uv: I32,
129    /// IBUS current in microamperes (µA), signed
130    pub ibus_ua: I32,
131    /// CC1 line voltage; units depend on the configured graph sample rate
132    pub cc1_raw: U16,
133    /// CC2 line voltage; units depend on the configured graph sample rate
134    pub cc2_raw: U16,
135    /// D+ line voltage; units depend on the configured graph sample rate
136    pub vdp_raw: U16,
137    /// D- line voltage; units depend on the configured graph sample rate
138    pub vdm_raw: U16,
139}
140
141/// Parsed AdcQueue sample with converted units
142#[derive(Debug, Clone, Copy, PartialEq)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueSample"))]
145pub struct AdcQueueSample {
146    pub sequence: u16,
147    pub vbus: ElectricPotential,
148    pub ibus: ElectricCurrent,
149    pub power: Power,
150    pub cc1: ElectricPotential,
151    pub cc2: ElectricPotential,
152    pub vdp: ElectricPotential,
153    pub vdm: ElectricPotential,
154}
155
156impl AdcQueueSample {
157    /// Convert a raw sample using the units for the configured graph rate.
158    pub fn from_raw(raw: AdcQueueSampleRaw, rate: GraphSampleRate) -> Self {
159        let vbus = ElectricPotential::new::<microvolt>(f64::from(raw.vbus_uv.get()));
160        let ibus = ElectricCurrent::new::<microampere>(f64::from(raw.ibus_ua.get()));
161        let auxiliary_voltage_lsb = rate.auxiliary_voltage_lsb();
162
163        Self {
164            sequence: raw.sequence.get(),
165            vbus,
166            ibus,
167            power: vbus * ibus,
168            cc1: auxiliary_voltage_lsb * f64::from(raw.cc1_raw.get()),
169            cc2: auxiliary_voltage_lsb * f64::from(raw.cc2_raw.get()),
170            vdp: auxiliary_voltage_lsb * f64::from(raw.vdp_raw.get()),
171            vdm: auxiliary_voltage_lsb * f64::from(raw.vdm_raw.get()),
172        }
173    }
174}
175
176/// Complete AdcQueue response containing multiple buffered samples
177#[derive(Debug, Clone, PartialEq)]
178#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
179#[cfg_attr(
180    feature = "python",
181    pyo3::pyclass(get_all, skip_from_py_object, name = "AdcQueueData")
182)]
183pub struct AdcQueueData {
184    pub samples: Vec<AdcQueueSample>,
185}
186
187impl AdcQueueData {
188    /// Parse AdcQueue payload containing multiple 20-byte samples
189    ///
190    /// Note: Some firmware versions may return payloads not divisible by 20.
191    /// In this case, we parse as many complete samples as possible.
192    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::KMError> {
193        let rate = Self::infer_rate(bytes).unwrap_or(GraphSampleRate::Sps2);
194        Self::from_bytes_with_rate(bytes, rate)
195    }
196
197    /// Parse AdcQueue payload using the explicitly configured graph rate.
198    ///
199    /// Prefer this method for live device traffic. [`Self::from_bytes`] infers
200    /// the rate from adjacent sequence values, but a one-sample payload does
201    /// not carry enough information and falls back to the 2 SPS scale.
202    pub fn from_bytes_with_rate(bytes: &[u8], rate: GraphSampleRate) -> Result<Self, crate::error::KMError> {
203        const SAMPLE_SIZE: usize = 20;
204
205        // Take as many complete samples as we can
206        let num_samples = bytes.len() / SAMPLE_SIZE;
207        let mut samples = Vec::with_capacity(num_samples);
208
209        for i in 0..num_samples {
210            let offset = i * SAMPLE_SIZE;
211            let sample_raw = AdcQueueSampleRaw::ref_from_bytes(&bytes[offset..offset + SAMPLE_SIZE])
212                .map_err(|_| crate::error::KMError::InvalidPacket("Failed to parse AdcQueue sample".to_string()))?;
213
214            samples.push(AdcQueueSample::from_raw(*sample_raw, rate));
215        }
216
217        Ok(Self { samples })
218    }
219
220    fn infer_rate(bytes: &[u8]) -> Option<GraphSampleRate> {
221        const SAMPLE_SIZE: usize = 20;
222
223        let mut sequences = bytes
224            .chunks_exact(SAMPLE_SIZE)
225            .filter_map(|sample| AdcQueueSampleRaw::ref_from_bytes(sample).ok())
226            .map(|sample| sample.sequence.get());
227        let mut previous = sequences.next()?;
228
229        for current in sequences {
230            let step = current.wrapping_sub(previous);
231            if let Some(rate) = GraphSampleRate::from_sequence_step(step) {
232                return Some(rate);
233            }
234            previous = current;
235        }
236
237        None
238    }
239
240    /// Get the sequence number range of samples in this queue
241    pub fn sequence_range(&self) -> Option<(u16, u16)> {
242        if self.samples.is_empty() {
243            None
244        } else {
245            Some((
246                self.samples.first().unwrap().sequence,
247                self.samples.last().unwrap().sequence,
248            ))
249        }
250    }
251
252    /// Check for dropped samples using the sequence step for `rate`.
253    pub fn has_dropped_samples(&self, rate: GraphSampleRate) -> bool {
254        self.samples
255            .windows(2)
256            .any(|window| rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
257    }
258}
259
260#[cfg(feature = "python")]
261#[pyo3::pymethods]
262impl AdcQueueData {
263    #[pyo3(name = "sequence_range")]
264    fn py_sequence_range(&self) -> Option<(u16, u16)> {
265        self.sequence_range()
266    }
267
268    #[pyo3(name = "has_dropped_samples")]
269    fn py_has_dropped_samples(&self, rate_index: u16) -> pyo3::PyResult<bool> {
270        let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
271            pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
272        })?;
273        Ok(self.has_dropped_samples(rate))
274    }
275
276    fn __repr__(&self) -> String {
277        format!("AdcQueueData({} samples)", self.samples.len())
278    }
279
280    fn __str__(&self) -> String {
281        self.__repr__()
282    }
283}
284
285#[cfg(feature = "python")]
286#[pyo3::pymethods]
287impl AdcQueueSample {
288    #[getter]
289    fn sequence(&self) -> u16 {
290        self.sequence
291    }
292    #[getter]
293    fn vbus_v(&self) -> f64 {
294        self.vbus.get::<volt>()
295    }
296    #[getter]
297    fn ibus_a(&self) -> f64 {
298        self.ibus.get::<ampere>()
299    }
300    #[getter]
301    fn power_w(&self) -> f64 {
302        self.power.get::<watt>()
303    }
304    #[getter]
305    fn cc1_v(&self) -> f64 {
306        self.cc1.get::<volt>()
307    }
308    #[getter]
309    fn cc2_v(&self) -> f64 {
310        self.cc2.get::<volt>()
311    }
312    #[getter]
313    fn vdp_v(&self) -> f64 {
314        self.vdp.get::<volt>()
315    }
316    #[getter]
317    fn vdm_v(&self) -> f64 {
318        self.vdm.get::<volt>()
319    }
320
321    fn __repr__(&self) -> String {
322        format!(
323            "AdcQueueSample(seq={}, vbus={:.3}V, ibus={:.3}A, power={:.3}W)",
324            self.sequence,
325            self.vbus.get::<volt>(),
326            self.ibus.get::<ampere>(),
327            self.power.get::<watt>()
328        )
329    }
330
331    fn __str__(&self) -> String {
332        self.__repr__()
333    }
334}