Skip to main content

km003c_lib/
adc.rs

1use num_enum::{IntoPrimitive, TryFromPrimitive};
2use std::fmt;
3use strum_macros::Display;
4use uom::si::electric_current::{ampere, microampere};
5use uom::si::electric_potential::{microvolt, millivolt, volt};
6use uom::si::f64::{ElectricCurrent, ElectricPotential, Frequency, Power, ThermodynamicTemperature};
7use uom::si::frequency::hertz;
8use uom::si::power::watt;
9use uom::si::thermodynamic_temperature::degree_celsius;
10use zerocopy::byteorder::little_endian::{I16, I32, U16};
11use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Default, TryFromPrimitive, IntoPrimitive)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18#[cfg_attr(feature = "python", pyo3::pyclass(eq, skip_from_py_object, name = "SampleRate"))]
19#[repr(u8)]
20pub enum SampleRate {
21    #[default]
22    #[strum(to_string = "2 SPS")]
23    Sps2 = 0,
24    #[strum(to_string = "10 SPS")]
25    Sps10 = 1,
26    #[strum(to_string = "50 SPS")]
27    Sps50 = 2,
28    #[strum(to_string = "1 kSPS")]
29    Sps1000 = 3,
30    #[strum(to_string = "10 kSPS")]
31    Sps10000 = 4,
32}
33
34impl SampleRate {
35    /// Get the sample rate in samples per second
36    pub fn frequency(self) -> Frequency {
37        Frequency::new::<hertz>(match self {
38            Self::Sps2 => 2.0,
39            Self::Sps10 => 10.0,
40            Self::Sps50 => 50.0,
41            Self::Sps1000 => 1_000.0,
42            Self::Sps10000 => 10_000.0,
43        })
44    }
45}
46
47#[cfg(feature = "python")]
48#[pyo3::pymethods]
49impl SampleRate {
50    #[getter]
51    fn hz(&self) -> f64 {
52        self.frequency().get::<hertz>()
53    }
54
55    #[getter]
56    fn name(&self) -> String {
57        self.to_string()
58    }
59
60    fn __repr__(&self) -> String {
61        format!("SampleRate({})", self)
62    }
63
64    fn __str__(&self) -> String {
65        self.to_string()
66    }
67}
68
69#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
70#[repr(C)]
71pub struct AdcDataRaw {
72    pub vbus_uv: I32,          // Microvolts (µV)
73    pub ibus_ua: I32,          // Microamps (µA)
74    pub vbus_avg_uv: I32,      // Microvolts (µV)
75    pub ibus_avg_ua: I32,      // Microamps (µA)
76    pub vbus_ori_avg_raw: I32, // Uncalibrated
77    pub ibus_ori_avg_raw: I32, // Uncalibrated
78    // Temperature register, LSB = 1/128 °C (7.8125 m°C)
79    pub temp_raw: I16,
80    pub vcc1_tenth_mv: U16,    // 0.1 millivolts
81    pub vcc2_raw: U16,         // 0.1 millivolts (instantaneous)
82    pub vdp_mv: U16,           // 0.1 millivolts (instantaneous)
83    pub vdm_mv: U16,           // 0.1 millivolts (instantaneous)
84    pub internal_vdd_raw: U16, // Internal VDD (0.1 millivolts)
85    pub rate_raw: u8,          // Sample rate index
86    pub reserved: u8,          // Vendor flags (observed 128)
87    pub vcc2_avg_raw: U16,     // 1 millivolt (averaged)
88    pub vdp_avg_mv: U16,       // 1 millivolt (averaged)
89    pub vdm_avg_mv: U16,       // 1 millivolt (averaged)
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "AdcData"))]
95pub struct AdcDataSimple {
96    // Main measurements
97    pub vbus: ElectricPotential,
98    /// Current in amperes. Sign indicates power flow direction through the tester:
99    /// - Positive: Power flows from USB female (input) to USB male (output)
100    /// - Negative: Power flows from USB male (input) to USB female (output)
101    pub ibus: ElectricCurrent,
102    /// Power in watts. Sign indicates power flow direction through the tester:
103    /// - Positive: Power flows from USB female (input) to USB male (output)
104    /// - Negative: Power flows from USB male (input) to USB female (output)
105    pub power: Power,
106
107    // Averaged measurements
108    pub vbus_average: ElectricPotential,
109    pub ibus_average: ElectricCurrent,
110    /// Uncalibrated VBUS average preserved verbatim from the device.
111    pub vbus_uncalibrated_average_raw: i32,
112    /// Uncalibrated IBUS average preserved verbatim from the device.
113    pub ibus_uncalibrated_average_raw: i32,
114
115    // Temperature
116    pub temperature: ThermodynamicTemperature,
117
118    // USB data lines
119    pub vdp: ElectricPotential,
120    pub vdm: ElectricPotential,
121    pub vdp_average: ElectricPotential,
122    pub vdm_average: ElectricPotential,
123
124    // USB CC lines
125    pub cc1: ElectricPotential,
126    pub cc2: ElectricPotential,
127    pub cc2_average: ElectricPotential,
128
129    // Internal voltage
130    pub internal_vdd: ElectricPotential,
131
132    /// Known sample rate, or `None` when the device reports an unknown index.
133    pub sample_rate: Option<SampleRate>,
134    /// Original sample-rate index, retained for lossless parsing and serialization.
135    pub sample_rate_raw: u8,
136    /// Opaque vendor flags preserved from the ADC payload.
137    pub vendor_flags: u8,
138}
139
140impl From<AdcDataRaw> for AdcDataSimple {
141    fn from(raw: AdcDataRaw) -> Self {
142        let vbus = ElectricPotential::new::<microvolt>(f64::from(raw.vbus_uv.get()));
143        let ibus = ElectricCurrent::new::<microampere>(f64::from(raw.ibus_ua.get()));
144
145        let sample_rate = SampleRate::try_from(raw.rate_raw).ok();
146
147        AdcDataSimple {
148            vbus,
149            ibus,
150            power: vbus * ibus,
151            vbus_average: ElectricPotential::new::<microvolt>(f64::from(raw.vbus_avg_uv.get())),
152            ibus_average: ElectricCurrent::new::<microampere>(f64::from(raw.ibus_avg_ua.get())),
153            vbus_uncalibrated_average_raw: raw.vbus_ori_avg_raw.get(),
154            ibus_uncalibrated_average_raw: raw.ibus_ori_avg_raw.get(),
155            temperature: ThermodynamicTemperature::new::<degree_celsius>(f64::from(raw.temp_raw.get()) / 128.0),
156            vdp: ElectricPotential::new::<millivolt>(f64::from(raw.vdp_mv.get()) / 10.0),
157            vdm: ElectricPotential::new::<millivolt>(f64::from(raw.vdm_mv.get()) / 10.0),
158            vdp_average: ElectricPotential::new::<millivolt>(f64::from(raw.vdp_avg_mv.get())),
159            vdm_average: ElectricPotential::new::<millivolt>(f64::from(raw.vdm_avg_mv.get())),
160            cc1: ElectricPotential::new::<millivolt>(f64::from(raw.vcc1_tenth_mv.get()) / 10.0),
161            cc2: ElectricPotential::new::<millivolt>(f64::from(raw.vcc2_raw.get()) / 10.0),
162            cc2_average: ElectricPotential::new::<millivolt>(f64::from(raw.vcc2_avg_raw.get())),
163            internal_vdd: ElectricPotential::new::<millivolt>(f64::from(raw.internal_vdd_raw.get()) / 10.0),
164            sample_rate,
165            sample_rate_raw: raw.rate_raw,
166            vendor_flags: raw.reserved,
167        }
168    }
169}
170
171impl From<AdcDataSimple> for AdcDataRaw {
172    fn from(data: AdcDataSimple) -> Self {
173        AdcDataRaw {
174            vbus_uv: I32::new(data.vbus.get::<microvolt>().round() as i32),
175            ibus_ua: I32::new(data.ibus.get::<microampere>().round() as i32),
176            vbus_avg_uv: I32::new(data.vbus_average.get::<microvolt>().round() as i32),
177            ibus_avg_ua: I32::new(data.ibus_average.get::<microampere>().round() as i32),
178            vbus_ori_avg_raw: I32::new(data.vbus_uncalibrated_average_raw),
179            ibus_ori_avg_raw: I32::new(data.ibus_uncalibrated_average_raw),
180            // Encode temperature back to raw register: °C * 128
181            temp_raw: I16::new((data.temperature.get::<degree_celsius>() * 128.0).round() as i16),
182            vcc1_tenth_mv: U16::new((data.cc1.get::<millivolt>() * 10.0).round() as u16),
183            vcc2_raw: U16::new((data.cc2.get::<millivolt>() * 10.0).round() as u16),
184            vdp_mv: U16::new((data.vdp.get::<millivolt>() * 10.0).round() as u16),
185            vdm_mv: U16::new((data.vdm.get::<millivolt>() * 10.0).round() as u16),
186            internal_vdd_raw: U16::new((data.internal_vdd.get::<millivolt>() * 10.0).round() as u16),
187            rate_raw: data.sample_rate.map_or(data.sample_rate_raw, u8::from),
188            reserved: data.vendor_flags,
189            // Store averaged fields in 1 mV units
190            vcc2_avg_raw: U16::new(data.cc2_average.get::<millivolt>().round() as u16),
191            vdp_avg_mv: U16::new(data.vdp_average.get::<millivolt>().round() as u16),
192            vdm_avg_mv: U16::new(data.vdm_average.get::<millivolt>().round() as u16),
193        }
194    }
195}
196
197impl AdcDataSimple {
198    /// Get the absolute current in amperes (magnitude regardless of direction)
199    pub fn current_abs(&self) -> ElectricCurrent {
200        self.ibus.abs()
201    }
202
203    /// Get the absolute power in watts (magnitude regardless of direction)
204    pub fn power_abs(&self) -> Power {
205        self.power.abs()
206    }
207}
208
209impl fmt::Display for AdcDataSimple {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(
212            f,
213            "VBUS: {:.3} V, IBUS: {:.3} A, Power: {:.3} W, Temp: {:.1} °C, Rate: {}",
214            self.vbus.get::<volt>(),
215            self.ibus.get::<ampere>(),
216            self.power.get::<watt>(),
217            self.temperature.get::<degree_celsius>(),
218            self.sample_rate.map_or_else(
219                || format!("Unknown ({})", self.sample_rate_raw),
220                |rate| rate.to_string()
221            )
222        )
223    }
224}
225
226#[cfg(feature = "python")]
227#[pyo3::pymethods]
228impl AdcDataSimple {
229    #[getter]
230    fn vbus_v(&self) -> f64 {
231        self.vbus.get::<volt>()
232    }
233    #[getter]
234    fn ibus_a(&self) -> f64 {
235        self.ibus.get::<ampere>()
236    }
237    #[getter]
238    fn power_w(&self) -> f64 {
239        self.power.get::<watt>()
240    }
241    #[getter]
242    fn vbus_avg_v(&self) -> f64 {
243        self.vbus_average.get::<volt>()
244    }
245    #[getter]
246    fn ibus_avg_a(&self) -> f64 {
247        self.ibus_average.get::<ampere>()
248    }
249    #[getter]
250    fn temp_c(&self) -> f64 {
251        self.temperature.get::<degree_celsius>()
252    }
253    #[getter]
254    fn vdp_v(&self) -> f64 {
255        self.vdp.get::<volt>()
256    }
257    #[getter]
258    fn vdm_v(&self) -> f64 {
259        self.vdm.get::<volt>()
260    }
261    #[getter]
262    fn vdp_avg_v(&self) -> f64 {
263        self.vdp_average.get::<volt>()
264    }
265    #[getter]
266    fn vdm_avg_v(&self) -> f64 {
267        self.vdm_average.get::<volt>()
268    }
269    #[getter]
270    fn cc1_v(&self) -> f64 {
271        self.cc1.get::<volt>()
272    }
273    #[getter]
274    fn cc2_v(&self) -> f64 {
275        self.cc2.get::<volt>()
276    }
277    #[getter]
278    fn cc2_avg_v(&self) -> f64 {
279        self.cc2_average.get::<volt>()
280    }
281    #[getter]
282    fn internal_vdd_v(&self) -> f64 {
283        self.internal_vdd.get::<volt>()
284    }
285    #[getter]
286    fn sample_rate(&self) -> Option<SampleRate> {
287        self.sample_rate
288    }
289    #[getter]
290    fn sample_rate_raw(&self) -> u8 {
291        self.sample_rate_raw
292    }
293    #[getter]
294    fn vendor_flags(&self) -> u8 {
295        self.vendor_flags
296    }
297    #[getter]
298    fn vbus_uncalibrated_average_raw(&self) -> i32 {
299        self.vbus_uncalibrated_average_raw
300    }
301    #[getter]
302    fn ibus_uncalibrated_average_raw(&self) -> i32 {
303        self.ibus_uncalibrated_average_raw
304    }
305
306    fn __repr__(&self) -> String {
307        format!(
308            "AdcData(vbus={:.3}V, ibus={:.3}A, power={:.3}W, temp={:.1}°C)",
309            self.vbus.get::<volt>(),
310            self.ibus.get::<ampere>(),
311            self.power.get::<watt>(),
312            self.temperature.get::<degree_celsius>()
313        )
314    }
315
316    fn __str__(&self) -> String {
317        self.__repr__()
318    }
319}