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#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[repr(u16)]
24pub enum GraphSampleRate {
25 Sps2 = 0,
27 Sps10 = 1,
29 Sps50 = 2,
31 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 pub const fn sequence_step(self) -> u16 {
49 Self::SEQUENCE_TICKS_PER_SECOND / self.sample_count_per_second()
50 }
51
52 pub fn frequency(self) -> Frequency {
54 Frequency::new::<hertz>(f64::from(self.sample_count_per_second()))
55 }
56
57 pub fn sequence_counter_frequency() -> Frequency {
59 Frequency::new::<hertz>(f64::from(Self::SEQUENCE_TICKS_PER_SECOND))
60 }
61
62 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 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 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
91pub 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#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
122#[repr(C)]
123struct AdcQueueSampleWire {
124 pub sequence: U16,
126 pub marker: U16,
128 pub vbus_uv: I32,
130 pub ibus_ua: I32,
132 pub cc1_raw: U16,
134 pub cc2_raw: U16,
136 pub vdp_raw: U16,
138 pub vdm_raw: U16,
140}
141
142#[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#[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 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 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#[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 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 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#[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 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 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 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 #[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}