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#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[repr(u16)]
24pub enum GraphSampleRate {
25    /// 2 samples per second
26    Sps2 = 0,
27    /// 10 samples per second
28    Sps10 = 1,
29    /// 50 samples per second
30    Sps50 = 2,
31    /// 1000 samples per second
32    Sps1000 = 3,
33}
34
35impl GraphSampleRate {
36    const SEQUENCE_TICKS_PER_SECOND: u16 = 1000;
37
38    const fn sample_count_per_second(self) -> u16 {
39        match self {
40            Self::Sps2 => 2,
41            Self::Sps10 => 10,
42            Self::Sps50 => 50,
43            Self::Sps1000 => 1000,
44        }
45    }
46
47    /// Expected increment of the device's 1000 Hz sequence counter.
48    pub const fn sequence_step(self) -> u16 {
49        Self::SEQUENCE_TICKS_PER_SECOND / self.sample_count_per_second()
50    }
51
52    /// Sampling frequency represented as a typed physical quantity.
53    pub fn frequency(self) -> Frequency {
54        Frequency::new::<hertz>(f64::from(self.sample_count_per_second()))
55    }
56
57    /// Frequency of the wrapping sequence counter used by graph samples.
58    pub fn sequence_counter_frequency() -> Frequency {
59        Frequency::new::<hertz>(f64::from(Self::SEQUENCE_TICKS_PER_SECOND))
60    }
61
62    /// Voltage represented by one auxiliary-line raw count.
63    ///
64    /// Recorded traffic and measurements against firmware 1.9.9 show that
65    /// 2 SPS uses 0.1 mV units, while faster graph modes use 1 mV units.
66    pub fn auxiliary_voltage_lsb(self) -> ElectricPotential {
67        match self {
68            Self::Sps2 => ElectricPotential::new::<millivolt>(0.1),
69            Self::Sps10 | Self::Sps50 | Self::Sps1000 => ElectricPotential::new::<millivolt>(1.0),
70        }
71    }
72
73    /// Infer the configured rate from a contiguous sequence-counter step.
74    pub const fn from_sequence_step(step: u16) -> Option<Self> {
75        match step {
76            500 => Some(Self::Sps2),
77            100 => Some(Self::Sps10),
78            20 => Some(Self::Sps50),
79            1 => Some(Self::Sps1000),
80            _ => None,
81        }
82    }
83
84    /// Number of samples missing between two sequence counter values.
85    pub fn missing_samples(self, previous: u16, current: u16) -> u16 {
86        let elapsed_ticks = current.wrapping_sub(previous);
87        elapsed_ticks.saturating_sub(self.sequence_step()) / self.sequence_step()
88    }
89}
90
91/// Elapsed device time between two values of the wrapping AdcQueue sequence counter.
92pub fn sequence_elapsed(previous: u16, current: u16) -> Time {
93    let elapsed_ticks = current.wrapping_sub(previous);
94    Time::new::<millisecond>(f64::from(elapsed_ticks))
95}
96
97impl fmt::Display for GraphSampleRate {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Sps2 => write!(f, "2 SPS"),
101            Self::Sps10 => write!(f, "10 SPS"),
102            Self::Sps50 => write!(f, "50 SPS"),
103            Self::Sps1000 => write!(f, "1000 SPS"),
104        }
105    }
106}
107
108/// AdcQueue sample structure (20 bytes)
109///
110/// AdcQueue provides high-rate streaming of power measurements.
111/// Each sample contains voltage/current measurements for all major lines
112/// (VBUS, IBUS, CC1, CC2, D+, D-) but without statistics or temperature.
113///
114/// Typical usage: Device buffers 5-48 samples and sends them in a single
115/// USB transfer for efficient high-rate logging (up to 1000 SPS).
116///
117/// Fields NOT included (unlike ADC):
118/// - Temperature (always request ADC separately for temp)
119/// - Statistics (min/max/avg)
120/// - VDD (internal voltage)
121#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
122#[repr(C)]
123struct AdcQueueSampleWire {
124    /// Incrementing sequence number for detecting dropped samples
125    pub sequence: U16,
126    /// Marker/flags field (varies by firmware version and mode, not temperature)
127    pub marker: U16,
128    /// VBUS voltage in microvolts (µV)
129    pub vbus_uv: I32,
130    /// IBUS current in microamperes (µA), signed
131    pub ibus_ua: I32,
132    /// CC1 line voltage; units depend on the configured graph sample rate
133    pub cc1_raw: U16,
134    /// CC2 line voltage; units depend on the configured graph sample rate
135    pub cc2_raw: U16,
136    /// D+ line voltage; units depend on the configured graph sample rate
137    pub vdp_raw: U16,
138    /// D- line voltage; units depend on the configured graph sample rate
139    pub vdm_raw: U16,
140}
141
142/// Lossless raw AdcQueue sample.
143///
144/// The four auxiliary-line fields are device counts rather than voltages.
145/// Converting them requires the sample rate selected by `StartGraph`.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
148#[cfg_attr(feature = "python", pyo3::pyclass(get_all, skip_from_py_object))]
149pub struct AdcQueueSampleRaw {
150    pub sequence: u16,
151    pub marker: u16,
152    pub vbus_uv: i32,
153    pub ibus_ua: i32,
154    pub cc1_raw: u16,
155    pub cc2_raw: u16,
156    pub vdp_raw: u16,
157    pub vdm_raw: u16,
158}
159
160impl From<AdcQueueSampleWire> for AdcQueueSampleRaw {
161    fn from(raw: AdcQueueSampleWire) -> Self {
162        Self {
163            sequence: raw.sequence.get(),
164            marker: raw.marker.get(),
165            vbus_uv: raw.vbus_uv.get(),
166            ibus_ua: raw.ibus_ua.get(),
167            cc1_raw: raw.cc1_raw.get(),
168            cc2_raw: raw.cc2_raw.get(),
169            vdp_raw: raw.vdp_raw.get(),
170            vdm_raw: raw.vdm_raw.get(),
171        }
172    }
173}
174
175impl From<AdcQueueSampleRaw> for AdcQueueSampleWire {
176    fn from(raw: AdcQueueSampleRaw) -> Self {
177        Self {
178            sequence: U16::new(raw.sequence),
179            marker: U16::new(raw.marker),
180            vbus_uv: I32::new(raw.vbus_uv),
181            ibus_ua: I32::new(raw.ibus_ua),
182            cc1_raw: U16::new(raw.cc1_raw),
183            cc2_raw: U16::new(raw.cc2_raw),
184            vdp_raw: U16::new(raw.vdp_raw),
185            vdm_raw: U16::new(raw.vdm_raw),
186        }
187    }
188}
189
190/// AdcQueue payload parsed without guessing the graph sample rate.
191#[derive(Debug, Clone, PartialEq, Eq)]
192#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
193#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object))]
194pub struct AdcQueueRawData {
195    pub samples: Vec<AdcQueueSampleRaw>,
196}
197
198impl AdcQueueRawData {
199    const SAMPLE_SIZE: usize = 20;
200
201    /// Parse a payload losslessly without assigning units to auxiliary counts.
202    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::KMError> {
203        if !bytes.len().is_multiple_of(Self::SAMPLE_SIZE) {
204            return Err(crate::error::KMError::InvalidPacket(format!(
205                "AdcQueue payload length must be a multiple of {}, got {}",
206                Self::SAMPLE_SIZE,
207                bytes.len()
208            )));
209        }
210
211        let samples = bytes
212            .chunks_exact(Self::SAMPLE_SIZE)
213            .map(|sample| {
214                AdcQueueSampleWire::ref_from_bytes(sample)
215                    .map(|wire| AdcQueueSampleRaw::from(*wire))
216                    .map_err(|_| crate::error::KMError::InvalidPacket("Failed to parse AdcQueue sample".to_string()))
217            })
218            .collect::<Result<_, _>>()?;
219
220        Ok(Self { samples })
221    }
222
223    /// Convert raw auxiliary counts using an explicitly known graph rate.
224    pub fn decode(&self, rate: GraphSampleRate) -> AdcQueueData {
225        AdcQueueData {
226            rate,
227            samples: self
228                .samples
229                .iter()
230                .copied()
231                .map(|sample| AdcQueueSample::from_raw(sample, rate))
232                .collect(),
233        }
234    }
235
236    pub fn sequence_range(&self) -> Option<(u16, u16)> {
237        self.samples
238            .first()
239            .zip(self.samples.last())
240            .map(|(first, last)| (first.sequence, last.sequence))
241    }
242
243    pub fn has_dropped_samples(&self, rate: GraphSampleRate) -> bool {
244        self.samples
245            .windows(2)
246            .any(|window| rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
247    }
248
249    pub fn to_bytes(&self) -> Vec<u8> {
250        self.samples
251            .iter()
252            .copied()
253            .flat_map(|sample| AdcQueueSampleWire::from(sample).as_bytes().to_vec())
254            .collect()
255    }
256}
257
258/// Parsed AdcQueue sample with converted units
259#[derive(Debug, Clone, Copy, PartialEq)]
260#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
261#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueSample"))]
262pub struct AdcQueueSample {
263    pub sequence: u16,
264    /// Opaque device marker/flags preserved from the wire sample.
265    pub marker: u16,
266    pub vbus: ElectricPotential,
267    pub ibus: ElectricCurrent,
268    pub power: Power,
269    pub cc1: ElectricPotential,
270    pub cc2: ElectricPotential,
271    pub vdp: ElectricPotential,
272    pub vdm: ElectricPotential,
273}
274
275impl AdcQueueSample {
276    /// Convert a raw sample using the units for the configured graph rate.
277    pub fn from_raw(raw: AdcQueueSampleRaw, rate: GraphSampleRate) -> Self {
278        let vbus = ElectricPotential::new::<microvolt>(f64::from(raw.vbus_uv));
279        let ibus = ElectricCurrent::new::<microampere>(f64::from(raw.ibus_ua));
280        let auxiliary_voltage_lsb = rate.auxiliary_voltage_lsb();
281
282        Self {
283            sequence: raw.sequence,
284            marker: raw.marker,
285            vbus,
286            ibus,
287            power: vbus * ibus,
288            cc1: auxiliary_voltage_lsb * f64::from(raw.cc1_raw),
289            cc2: auxiliary_voltage_lsb * f64::from(raw.cc2_raw),
290            vdp: auxiliary_voltage_lsb * f64::from(raw.vdp_raw),
291            vdm: auxiliary_voltage_lsb * f64::from(raw.vdm_raw),
292        }
293    }
294}
295
296/// Complete AdcQueue response containing multiple buffered samples
297#[derive(Debug, Clone, PartialEq)]
298#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
299#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueData"))]
300pub struct AdcQueueData {
301    pub rate: GraphSampleRate,
302    pub samples: Vec<AdcQueueSample>,
303}
304
305impl AdcQueueData {
306    /// Parse AdcQueue payload using the explicitly configured graph rate.
307    pub fn from_bytes_with_rate(bytes: &[u8], rate: GraphSampleRate) -> Result<Self, crate::error::KMError> {
308        Ok(AdcQueueRawData::from_bytes(bytes)?.decode(rate))
309    }
310
311    /// Get the sequence number range of samples in this queue
312    pub fn sequence_range(&self) -> Option<(u16, u16)> {
313        self.samples
314            .first()
315            .zip(self.samples.last())
316            .map(|(first, last)| (first.sequence, last.sequence))
317    }
318
319    /// Check for dropped samples using the configured graph rate.
320    pub fn has_dropped_samples(&self) -> bool {
321        self.samples
322            .windows(2)
323            .any(|window| self.rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
324    }
325}
326
327#[cfg(feature = "python")]
328#[pyo3::pymethods]
329impl AdcQueueRawData {
330    #[getter]
331    fn samples(&self) -> Vec<AdcQueueSampleRaw> {
332        self.samples.clone()
333    }
334
335    #[pyo3(name = "sequence_range")]
336    fn py_sequence_range(&self) -> Option<(u16, u16)> {
337        self.sequence_range()
338    }
339
340    #[pyo3(name = "has_dropped_samples")]
341    fn py_has_dropped_samples(&self, rate_index: u16) -> pyo3::PyResult<bool> {
342        let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
343            pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
344        })?;
345        Ok(self.has_dropped_samples(rate))
346    }
347
348    /// Decode auxiliary-line counts using an explicitly known graph rate.
349    #[pyo3(name = "decode")]
350    fn py_decode(&self, rate_index: u16) -> pyo3::PyResult<AdcQueueData> {
351        let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
352            pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
353        })?;
354        Ok(self.decode(rate))
355    }
356
357    fn __repr__(&self) -> String {
358        format!("AdcQueueRawData({} samples)", self.samples.len())
359    }
360
361    fn __str__(&self) -> String {
362        self.__repr__()
363    }
364}
365
366#[cfg(feature = "python")]
367#[pyo3::pymethods]
368impl AdcQueueData {
369    #[getter]
370    fn rate_index(&self) -> u16 {
371        self.rate as u16
372    }
373
374    #[getter]
375    fn samples(&self) -> Vec<AdcQueueSample> {
376        self.samples.clone()
377    }
378
379    #[pyo3(name = "sequence_range")]
380    fn py_sequence_range(&self) -> Option<(u16, u16)> {
381        self.sequence_range()
382    }
383
384    #[pyo3(name = "has_dropped_samples")]
385    fn py_has_dropped_samples(&self) -> bool {
386        self.has_dropped_samples()
387    }
388
389    fn __repr__(&self) -> String {
390        format!("AdcQueueData({} samples)", self.samples.len())
391    }
392
393    fn __str__(&self) -> String {
394        self.__repr__()
395    }
396}
397
398#[cfg(feature = "python")]
399#[pyo3::pymethods]
400impl AdcQueueSample {
401    #[getter]
402    fn sequence(&self) -> u16 {
403        self.sequence
404    }
405    #[getter]
406    fn marker(&self) -> u16 {
407        self.marker
408    }
409    #[getter]
410    fn vbus_v(&self) -> f64 {
411        self.vbus.get::<volt>()
412    }
413    #[getter]
414    fn ibus_a(&self) -> f64 {
415        self.ibus.get::<ampere>()
416    }
417    #[getter]
418    fn power_w(&self) -> f64 {
419        self.power.get::<watt>()
420    }
421    #[getter]
422    fn cc1_v(&self) -> f64 {
423        self.cc1.get::<volt>()
424    }
425    #[getter]
426    fn cc2_v(&self) -> f64 {
427        self.cc2.get::<volt>()
428    }
429    #[getter]
430    fn vdp_v(&self) -> f64 {
431        self.vdp.get::<volt>()
432    }
433    #[getter]
434    fn vdm_v(&self) -> f64 {
435        self.vdm.get::<volt>()
436    }
437
438    fn __repr__(&self) -> String {
439        format!(
440            "AdcQueueSample(seq={}, vbus={:.3}V, ibus={:.3}A, power={:.3}W)",
441            self.sequence,
442            self.vbus.get::<volt>(),
443            self.ibus.get::<ampere>(),
444            self.power.get::<watt>()
445        )
446    }
447
448    fn __str__(&self) -> String {
449        self.__repr__()
450    }
451}