use num_enum::TryFromPrimitive;
use std::fmt;
use uom::si::electric_current::microampere;
use uom::si::electric_potential::{microvolt, millivolt};
use uom::si::f64::{ElectricCurrent, ElectricPotential, Frequency, Power, Time};
use uom::si::frequency::hertz;
use uom::si::time::millisecond;
use zerocopy::byteorder::little_endian::{I32, U16};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "python")]
use uom::si::{electric_current::ampere, electric_potential::volt, power::watt};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u16)]
pub enum GraphSampleRate {
Sps2 = 0,
Sps10 = 1,
Sps50 = 2,
Sps1000 = 3,
}
impl GraphSampleRate {
const SEQUENCE_TICKS_PER_SECOND: u16 = 1000;
const fn sample_count_per_second(self) -> u16 {
match self {
Self::Sps2 => 2,
Self::Sps10 => 10,
Self::Sps50 => 50,
Self::Sps1000 => 1000,
}
}
pub const fn sequence_step(self) -> u16 {
Self::SEQUENCE_TICKS_PER_SECOND / self.sample_count_per_second()
}
pub fn frequency(self) -> Frequency {
Frequency::new::<hertz>(f64::from(self.sample_count_per_second()))
}
pub fn sequence_counter_frequency() -> Frequency {
Frequency::new::<hertz>(f64::from(Self::SEQUENCE_TICKS_PER_SECOND))
}
pub fn auxiliary_voltage_lsb(self) -> ElectricPotential {
match self {
Self::Sps2 => ElectricPotential::new::<millivolt>(0.1),
Self::Sps10 | Self::Sps50 | Self::Sps1000 => ElectricPotential::new::<millivolt>(1.0),
}
}
pub const fn from_sequence_step(step: u16) -> Option<Self> {
match step {
500 => Some(Self::Sps2),
100 => Some(Self::Sps10),
20 => Some(Self::Sps50),
1 => Some(Self::Sps1000),
_ => None,
}
}
pub fn missing_samples(self, previous: u16, current: u16) -> u16 {
let elapsed_ticks = current.wrapping_sub(previous);
elapsed_ticks.saturating_sub(self.sequence_step()) / self.sequence_step()
}
}
pub fn sequence_elapsed(previous: u16, current: u16) -> Time {
let elapsed_ticks = current.wrapping_sub(previous);
Time::new::<millisecond>(f64::from(elapsed_ticks))
}
impl fmt::Display for GraphSampleRate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Sps2 => write!(f, "2 SPS"),
Self::Sps10 => write!(f, "10 SPS"),
Self::Sps50 => write!(f, "50 SPS"),
Self::Sps1000 => write!(f, "1000 SPS"),
}
}
}
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C)]
pub struct AdcQueueSampleRaw {
pub sequence: U16,
pub marker: U16,
pub vbus_uv: I32,
pub ibus_ua: I32,
pub cc1_raw: U16,
pub cc2_raw: U16,
pub vdp_raw: U16,
pub vdm_raw: U16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcQueueSample"))]
pub struct AdcQueueSample {
pub sequence: u16,
pub vbus: ElectricPotential,
pub ibus: ElectricCurrent,
pub power: Power,
pub cc1: ElectricPotential,
pub cc2: ElectricPotential,
pub vdp: ElectricPotential,
pub vdm: ElectricPotential,
}
impl AdcQueueSample {
pub fn from_raw(raw: AdcQueueSampleRaw, rate: GraphSampleRate) -> Self {
let vbus = ElectricPotential::new::<microvolt>(f64::from(raw.vbus_uv.get()));
let ibus = ElectricCurrent::new::<microampere>(f64::from(raw.ibus_ua.get()));
let auxiliary_voltage_lsb = rate.auxiliary_voltage_lsb();
Self {
sequence: raw.sequence.get(),
vbus,
ibus,
power: vbus * ibus,
cc1: auxiliary_voltage_lsb * f64::from(raw.cc1_raw.get()),
cc2: auxiliary_voltage_lsb * f64::from(raw.cc2_raw.get()),
vdp: auxiliary_voltage_lsb * f64::from(raw.vdp_raw.get()),
vdm: auxiliary_voltage_lsb * f64::from(raw.vdm_raw.get()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "python",
pyo3::pyclass(get_all, skip_from_py_object, name = "AdcQueueData")
)]
pub struct AdcQueueData {
pub samples: Vec<AdcQueueSample>,
}
impl AdcQueueData {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::KMError> {
let rate = Self::infer_rate(bytes).unwrap_or(GraphSampleRate::Sps2);
Self::from_bytes_with_rate(bytes, rate)
}
pub fn from_bytes_with_rate(bytes: &[u8], rate: GraphSampleRate) -> Result<Self, crate::error::KMError> {
const SAMPLE_SIZE: usize = 20;
let num_samples = bytes.len() / SAMPLE_SIZE;
let mut samples = Vec::with_capacity(num_samples);
for i in 0..num_samples {
let offset = i * SAMPLE_SIZE;
let sample_raw = AdcQueueSampleRaw::ref_from_bytes(&bytes[offset..offset + SAMPLE_SIZE])
.map_err(|_| crate::error::KMError::InvalidPacket("Failed to parse AdcQueue sample".to_string()))?;
samples.push(AdcQueueSample::from_raw(*sample_raw, rate));
}
Ok(Self { samples })
}
fn infer_rate(bytes: &[u8]) -> Option<GraphSampleRate> {
const SAMPLE_SIZE: usize = 20;
let mut sequences = bytes
.chunks_exact(SAMPLE_SIZE)
.filter_map(|sample| AdcQueueSampleRaw::ref_from_bytes(sample).ok())
.map(|sample| sample.sequence.get());
let mut previous = sequences.next()?;
for current in sequences {
let step = current.wrapping_sub(previous);
if let Some(rate) = GraphSampleRate::from_sequence_step(step) {
return Some(rate);
}
previous = current;
}
None
}
pub fn sequence_range(&self) -> Option<(u16, u16)> {
if self.samples.is_empty() {
None
} else {
Some((
self.samples.first().unwrap().sequence,
self.samples.last().unwrap().sequence,
))
}
}
pub fn has_dropped_samples(&self, rate: GraphSampleRate) -> bool {
self.samples
.windows(2)
.any(|window| rate.missing_samples(window[0].sequence, window[1].sequence) > 0)
}
}
#[cfg(feature = "python")]
#[pyo3::pymethods]
impl AdcQueueData {
#[pyo3(name = "sequence_range")]
fn py_sequence_range(&self) -> Option<(u16, u16)> {
self.sequence_range()
}
#[pyo3(name = "has_dropped_samples")]
fn py_has_dropped_samples(&self, rate_index: u16) -> pyo3::PyResult<bool> {
let rate = GraphSampleRate::try_from(rate_index).map_err(|_| {
pyo3::exceptions::PyValueError::new_err(format!("Invalid graph sample rate index: {rate_index}"))
})?;
Ok(self.has_dropped_samples(rate))
}
fn __repr__(&self) -> String {
format!("AdcQueueData({} samples)", self.samples.len())
}
fn __str__(&self) -> String {
self.__repr__()
}
}
#[cfg(feature = "python")]
#[pyo3::pymethods]
impl AdcQueueSample {
#[getter]
fn sequence(&self) -> u16 {
self.sequence
}
#[getter]
fn vbus_v(&self) -> f64 {
self.vbus.get::<volt>()
}
#[getter]
fn ibus_a(&self) -> f64 {
self.ibus.get::<ampere>()
}
#[getter]
fn power_w(&self) -> f64 {
self.power.get::<watt>()
}
#[getter]
fn cc1_v(&self) -> f64 {
self.cc1.get::<volt>()
}
#[getter]
fn cc2_v(&self) -> f64 {
self.cc2.get::<volt>()
}
#[getter]
fn vdp_v(&self) -> f64 {
self.vdp.get::<volt>()
}
#[getter]
fn vdm_v(&self) -> f64 {
self.vdm.get::<volt>()
}
fn __repr__(&self) -> String {
format!(
"AdcQueueSample(seq={}, vbus={:.3}V, ibus={:.3}A, power={:.3}W)",
self.sequence,
self.vbus.get::<volt>(),
self.ibus.get::<ampere>(),
self.power.get::<watt>()
)
}
fn __str__(&self) -> String {
self.__repr__()
}
}