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            .as_chunks::<{ Self::SAMPLE_SIZE }>()
213            .0
214            .iter()
215            .map(|sample| {
216                AdcQueueSampleWire::ref_from_bytes(sample)
217                    .map(|wire| AdcQueueSampleRaw::from(*wire))
218                    .map_err(|_| crate::error::KMError::InvalidPacket("Failed to parse AdcQueue sample".to_string()))
219            })
220            .collect::<Result<_, _>>()?;
221
222        Ok(Self { samples })
223    }
224
225    /// Convert raw auxiliary counts using an explicitly known graph rate.
226    pub fn decode(&self, rate: GraphSampleRate) -> AdcQueueData {
227        AdcQueueData {
228            rate,
229            samples: self
230                .samples
231                .iter()
232                .copied()
233                .map(|sample| AdcQueueSample::from_raw(sample, rate))
234                .collect(),
235        }
236    }
237
238    pub fn sequence_range(&self) -> Option<(u16, u16)> {
239        self.samples
240            .first()
241            .zip(self.samples.last())
242            .map(|(first, last)| (first.sequence, last.sequence))
243    }
244
245    pub fn has_dropped_samples(&self, rate: GraphSampleRate) -> bool {
246        self.samples
247            .windows(2)
248            .any(|window| rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
249    }
250
251    pub fn to_bytes(&self) -> Vec<u8> {
252        let mut bytes = Vec::with_capacity(self.samples.len() * Self::SAMPLE_SIZE);
253        for sample in self.samples.iter().copied() {
254            bytes.extend_from_slice(AdcQueueSampleWire::from(sample).as_bytes());
255        }
256        bytes
257    }
258}
259
260/// Parsed AdcQueue sample with converted units
261#[derive(Debug, Clone, Copy, PartialEq)]
262#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
263#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueSample"))]
264pub struct AdcQueueSample {
265    pub sequence: u16,
266    /// Opaque device marker/flags preserved from the wire sample.
267    pub marker: u16,
268    pub vbus: ElectricPotential,
269    pub ibus: ElectricCurrent,
270    pub power: Power,
271    pub cc1: ElectricPotential,
272    pub cc2: ElectricPotential,
273    pub vdp: ElectricPotential,
274    pub vdm: ElectricPotential,
275}
276
277impl AdcQueueSample {
278    /// Convert a raw sample using the units for the configured graph rate.
279    pub fn from_raw(raw: AdcQueueSampleRaw, rate: GraphSampleRate) -> Self {
280        let vbus = ElectricPotential::new::<microvolt>(f64::from(raw.vbus_uv));
281        let ibus = ElectricCurrent::new::<microampere>(f64::from(raw.ibus_ua));
282        let auxiliary_voltage_lsb = rate.auxiliary_voltage_lsb();
283
284        Self {
285            sequence: raw.sequence,
286            marker: raw.marker,
287            vbus,
288            ibus,
289            power: vbus * ibus,
290            cc1: auxiliary_voltage_lsb * f64::from(raw.cc1_raw),
291            cc2: auxiliary_voltage_lsb * f64::from(raw.cc2_raw),
292            vdp: auxiliary_voltage_lsb * f64::from(raw.vdp_raw),
293            vdm: auxiliary_voltage_lsb * f64::from(raw.vdm_raw),
294        }
295    }
296}
297
298/// Complete AdcQueue response containing multiple buffered samples
299#[derive(Debug, Clone, PartialEq)]
300#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
301#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueData"))]
302pub struct AdcQueueData {
303    pub rate: GraphSampleRate,
304    pub samples: Vec<AdcQueueSample>,
305}
306
307impl AdcQueueData {
308    /// Parse AdcQueue payload using the explicitly configured graph rate.
309    pub fn from_bytes_with_rate(bytes: &[u8], rate: GraphSampleRate) -> Result<Self, crate::error::KMError> {
310        Ok(AdcQueueRawData::from_bytes(bytes)?.decode(rate))
311    }
312
313    /// Get the sequence number range of samples in this queue
314    pub fn sequence_range(&self) -> Option<(u16, u16)> {
315        self.samples
316            .first()
317            .zip(self.samples.last())
318            .map(|(first, last)| (first.sequence, last.sequence))
319    }
320
321    /// Check for dropped samples using the configured graph rate.
322    pub fn has_dropped_samples(&self) -> bool {
323        self.samples
324            .windows(2)
325            .any(|window| self.rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
326    }
327}
328
329#[cfg(feature = "python")]
330#[pyo3::pymethods]
331impl AdcQueueRawData {
332    #[getter]
333    fn samples(&self) -> Vec<AdcQueueSampleRaw> {
334        self.samples.clone()
335    }
336
337    #[pyo3(name = "sequence_range")]
338    fn py_sequence_range(&self) -> Option<(u16, u16)> {
339        self.sequence_range()
340    }
341
342    #[pyo3(name = "has_dropped_samples")]
343    fn py_has_dropped_samples(&self, rate_index: u16) -> pyo3::PyResult<bool> {
344        let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
345            pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
346        })?;
347        Ok(self.has_dropped_samples(rate))
348    }
349
350    /// Decode auxiliary-line counts using an explicitly known graph rate.
351    #[pyo3(name = "decode")]
352    fn py_decode(&self, rate_index: u16) -> pyo3::PyResult<AdcQueueData> {
353        let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
354            pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
355        })?;
356        Ok(self.decode(rate))
357    }
358
359    fn __repr__(&self) -> String {
360        format!("AdcQueueRawData({} samples)", self.samples.len())
361    }
362
363    fn __str__(&self) -> String {
364        self.__repr__()
365    }
366}
367
368#[cfg(feature = "python")]
369#[pyo3::pymethods]
370impl AdcQueueData {
371    #[getter]
372    fn rate_index(&self) -> u16 {
373        self.rate as u16
374    }
375
376    #[getter]
377    fn samples(&self) -> Vec<AdcQueueSample> {
378        self.samples.clone()
379    }
380
381    #[pyo3(name = "sequence_range")]
382    fn py_sequence_range(&self) -> Option<(u16, u16)> {
383        self.sequence_range()
384    }
385
386    #[pyo3(name = "has_dropped_samples")]
387    fn py_has_dropped_samples(&self) -> bool {
388        self.has_dropped_samples()
389    }
390
391    fn __repr__(&self) -> String {
392        format!("AdcQueueData({} samples)", self.samples.len())
393    }
394
395    fn __str__(&self) -> String {
396        self.__repr__()
397    }
398}
399
400#[cfg(feature = "python")]
401#[pyo3::pymethods]
402impl AdcQueueSample {
403    #[getter]
404    fn sequence(&self) -> u16 {
405        self.sequence
406    }
407    #[getter]
408    fn marker(&self) -> u16 {
409        self.marker
410    }
411    #[getter]
412    fn vbus_v(&self) -> f64 {
413        self.vbus.get::<volt>()
414    }
415    #[getter]
416    fn ibus_a(&self) -> f64 {
417        self.ibus.get::<ampere>()
418    }
419    #[getter]
420    fn power_w(&self) -> f64 {
421        self.power.get::<watt>()
422    }
423    #[getter]
424    fn cc1_v(&self) -> f64 {
425        self.cc1.get::<volt>()
426    }
427    #[getter]
428    fn cc2_v(&self) -> f64 {
429        self.cc2.get::<volt>()
430    }
431    #[getter]
432    fn vdp_v(&self) -> f64 {
433        self.vdp.get::<volt>()
434    }
435    #[getter]
436    fn vdm_v(&self) -> f64 {
437        self.vdm.get::<volt>()
438    }
439
440    fn __repr__(&self) -> String {
441        format!(
442            "AdcQueueSample(seq={}, vbus={:.3}V, ibus={:.3}A, power={:.3}W)",
443            self.sequence,
444            self.vbus.get::<volt>(),
445            self.ibus.get::<ampere>(),
446            self.power.get::<watt>()
447        )
448    }
449
450    fn __str__(&self) -> String {
451        self.__repr__()
452    }
453}