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#[repr(u16)]
23pub enum GraphSampleRate {
24 Sps2 = 0,
26 Sps10 = 1,
28 Sps50 = 2,
30 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 pub const fn sequence_step(self) -> u16 {
48 Self::SEQUENCE_TICKS_PER_SECOND / self.sample_count_per_second()
49 }
50
51 pub fn frequency(self) -> Frequency {
53 Frequency::new::<hertz>(f64::from(self.sample_count_per_second()))
54 }
55
56 pub fn sequence_counter_frequency() -> Frequency {
58 Frequency::new::<hertz>(f64::from(Self::SEQUENCE_TICKS_PER_SECOND))
59 }
60
61 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 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 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
90pub 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#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
121#[repr(C)]
122pub struct AdcQueueSampleRaw {
123 pub sequence: U16,
125 pub marker: U16,
127 pub vbus_uv: I32,
129 pub ibus_ua: I32,
131 pub cc1_raw: U16,
133 pub cc2_raw: U16,
135 pub vdp_raw: U16,
137 pub vdm_raw: U16,
139}
140
141#[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 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#[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 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 pub fn from_bytes_with_rate(bytes: &[u8], rate: GraphSampleRate) -> Result<Self, crate::error::KMError> {
203 const SAMPLE_SIZE: usize = 20;
204
205 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 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 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}