Skip to main content

km003c_lib/
pd.rs

1use bytes::Bytes;
2use uom::si::electric_current::milliampere;
3use uom::si::electric_potential::millivolt;
4use uom::si::f64::{ElectricCurrent, ElectricPotential, Time};
5use uom::si::time::millisecond;
6use zerocopy::byteorder::little_endian::{I16, U16, U32};
7use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
8
9use crate::constants::*;
10use crate::error::KMError;
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15#[cfg(feature = "python")]
16use uom::si::{electric_current::ampere, electric_potential::volt};
17
18/// PD measurement block (12 bytes).
19///
20/// The device uses this exact layout both as a standalone/chained PD status
21/// payload and as the preamble of a PD event stream.
22#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
23#[repr(C)]
24pub struct PdStatusRaw {
25    pub timestamp_ms: U32,
26    pub vbus_mv: U16,
27    pub ibus_ma: I16, // Signed! Negative = power flowing from male to female port
28    pub cc1_mv: U16,
29    pub cc2_mv: U16,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object))]
35pub struct PdStatus {
36    pub timestamp: Time,
37    pub vbus: ElectricPotential,
38    pub ibus: ElectricCurrent,
39    pub cc1: ElectricPotential,
40    pub cc2: ElectricPotential,
41}
42
43impl From<PdStatusRaw> for PdStatus {
44    fn from(raw: PdStatusRaw) -> Self {
45        Self {
46            timestamp: Time::new::<millisecond>(f64::from(raw.timestamp_ms.get())),
47            vbus: ElectricPotential::new::<millivolt>(f64::from(raw.vbus_mv.get())),
48            ibus: ElectricCurrent::new::<milliampere>(f64::from(raw.ibus_ma.get())),
49            cc1: ElectricPotential::new::<millivolt>(f64::from(raw.cc1_mv.get())),
50            cc2: ElectricPotential::new::<millivolt>(f64::from(raw.cc2_mv.get())),
51        }
52    }
53}
54
55impl From<PdStatus> for PdStatusRaw {
56    fn from(status: PdStatus) -> Self {
57        // Rounding, not truncation, so a value that survived a float round trip
58        // re-encodes to the integer it came from.
59        Self {
60            timestamp_ms: U32::new(status.timestamp.get::<millisecond>().round() as u32),
61            vbus_mv: U16::new(status.vbus.get::<millivolt>().round() as u16),
62            ibus_ma: I16::new(status.ibus.get::<milliampere>().round() as i16),
63            cc1_mv: U16::new(status.cc1.get::<millivolt>().round() as u16),
64            cc2_mv: U16::new(status.cc2.get::<millivolt>().round() as u16),
65        }
66    }
67}
68
69#[cfg(feature = "python")]
70#[pyo3::pymethods]
71impl PdStatus {
72    #[getter]
73    fn timestamp(&self) -> f64 {
74        self.timestamp.get::<millisecond>()
75    }
76    #[getter]
77    fn vbus_v(&self) -> f64 {
78        self.vbus.get::<volt>()
79    }
80    #[getter]
81    fn ibus_a(&self) -> f64 {
82        self.ibus.get::<ampere>()
83    }
84    #[getter]
85    fn cc1_v(&self) -> f64 {
86        self.cc1.get::<volt>()
87    }
88    #[getter]
89    fn cc2_v(&self) -> f64 {
90        self.cc2.get::<volt>()
91    }
92
93    fn __repr__(&self) -> String {
94        format!(
95            "PdStatus(timestamp={}ms, vbus={:.3}V, ibus={:.3}A)",
96            self.timestamp.get::<millisecond>(),
97            self.vbus.get::<volt>(),
98            self.ibus.get::<ampere>()
99        )
100    }
101
102    fn __str__(&self) -> String {
103        self.__repr__()
104    }
105}
106
107/// Event data types that can appear in PD stream
108#[derive(Debug, Clone, PartialEq)]
109pub enum PdEventData {
110    Connect,
111    Disconnect,
112    PdMessage { sop: u8, wire_data: Vec<u8> },
113}
114
115#[cfg(feature = "python")]
116impl<'py> pyo3::IntoPyObject<'py> for PdEventData {
117    type Target = pyo3::types::PyDict;
118    type Output = pyo3::Bound<'py, Self::Target>;
119    type Error = pyo3::PyErr;
120
121    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
122        use pyo3::types::{PyDict, PyDictMethods};
123
124        let dict = PyDict::new(py);
125        match self {
126            Self::Connect => dict.set_item("Connect", py.None())?,
127            Self::Disconnect => dict.set_item("Disconnect", py.None())?,
128            Self::PdMessage { sop, wire_data } => {
129                let inner = PyDict::new(py);
130                inner.set_item("sop", sop)?;
131                inner.set_item("wire_data", wire_data)?;
132                dict.set_item("PdMessage", inner)?;
133            }
134        }
135        Ok(dict)
136    }
137}
138
139/// Timestamped PD event
140#[derive(Debug, Clone, PartialEq)]
141#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "PdEvent"))]
142pub struct PdEvent {
143    pub timestamp: Time,
144    pub data: PdEventData,
145}
146
147/// Complete PD event stream with preamble and events
148#[derive(Debug, Clone, PartialEq)]
149#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "PdEventStream"))]
150pub struct PdEventStream {
151    /// Measurements captured immediately before the event records.
152    pub preamble: PdStatus,
153    pub events: Vec<PdEvent>,
154}
155
156impl PdEventStream {
157    /// Parse PD event stream from bytes
158    /// Expected format: 12-byte preamble + repeated (6-byte header + wire data) events
159    pub fn from_bytes(bytes: Bytes) -> Result<Self, KMError> {
160        if bytes.len() < PD_STATUS_SIZE {
161            return Err(KMError::InvalidPacket(format!(
162                "PD event stream too short for preamble: need {}, got {}",
163                PD_STATUS_SIZE,
164                bytes.len()
165            )));
166        }
167
168        // Parse preamble
169        let preamble_raw = PdStatusRaw::ref_from_bytes(&bytes[..PD_STATUS_SIZE])
170            .map_err(|_| KMError::InvalidPacket("Failed to parse PD preamble".to_string()))?;
171        let preamble = PdStatus::from(*preamble_raw);
172
173        let mut events = Vec::new();
174        let mut offset = PD_STATUS_SIZE;
175
176        // Parse events
177        while offset < bytes.len() {
178            if bytes.len() - offset < PD_EVENT_HEADER_SIZE {
179                return Err(KMError::InvalidPacket(format!(
180                    "Incomplete PD event header at offset {}: need {}, got {}",
181                    offset,
182                    PD_EVENT_HEADER_SIZE,
183                    bytes.len() - offset
184                )));
185            }
186
187            let size_flag = bytes[offset];
188
189            // Connection/status event (0x45): 6-byte header with ts24 + code
190            if size_flag == PD_EVENT_TYPE_CONNECTION {
191                let ts24 = u32::from_le_bytes([bytes[offset + 1], bytes[offset + 2], bytes[offset + 3], 0]);
192                let event_code = bytes[offset + 5];
193                let data = match event_code {
194                    PD_CONNECTION_CONNECT | PD_CONNECTION_CONNECT_LEGACY => PdEventData::Connect,
195                    PD_CONNECTION_DISCONNECT | PD_CONNECTION_DISCONNECT_LEGACY => PdEventData::Disconnect,
196                    _ => PdEventData::PdMessage {
197                        sop: event_code,
198                        wire_data: Vec::new(),
199                    },
200                };
201                events.push(PdEvent {
202                    timestamp: Time::new::<millisecond>(f64::from(ts24)),
203                    data,
204                });
205                offset += PD_EVENT_HEADER_SIZE;
206                continue;
207            }
208
209            let timestamp = u32::from_le_bytes([
210                bytes[offset + 1],
211                bytes[offset + 2],
212                bytes[offset + 3],
213                bytes[offset + 4],
214            ]);
215            let sop = bytes[offset + 5];
216
217            // Calculate wire data length: wire_len = (size_flag & mask) - offset
218            let encoded_size = size_flag & PD_EVENT_SIZE_MASK;
219            let wire_len = encoded_size.checked_sub(PD_EVENT_SIZE_OFFSET).ok_or_else(|| {
220                KMError::InvalidPacket(format!(
221                    "Invalid PD event size at offset {}: encoded size {} is below {}",
222                    offset, encoded_size, PD_EVENT_SIZE_OFFSET
223                ))
224            })? as usize;
225
226            offset += PD_EVENT_HEADER_SIZE;
227
228            if bytes.len() - offset < wire_len {
229                return Err(KMError::InvalidPacket(format!(
230                    "Insufficient bytes for wire data: expected {}, got {}",
231                    wire_len,
232                    bytes.len() - offset
233                )));
234            }
235
236            let wire_data = if wire_len > 0 {
237                bytes.slice(offset..offset + wire_len).to_vec()
238            } else {
239                Vec::new()
240            };
241            offset += wire_len;
242
243            let data = PdEventData::PdMessage { sop, wire_data };
244
245            events.push(PdEvent {
246                timestamp: Time::new::<millisecond>(f64::from(timestamp)),
247                data,
248            });
249        }
250
251        Ok(Self { preamble, events })
252    }
253
254    /// Helper: get all PD messages, ignoring connection events
255    pub fn pd_messages(&self) -> impl Iterator<Item = (&Time, u8, &[u8])> {
256        self.events.iter().filter_map(|e| match &e.data {
257            PdEventData::PdMessage { sop, wire_data } => Some((&e.timestamp, *sop, wire_data.as_slice())),
258            _ => None,
259        })
260    }
261
262    /// Helper: get connection state changes
263    pub fn connection_events(&self) -> impl Iterator<Item = (&Time, bool)> {
264        self.events.iter().filter_map(|e| match &e.data {
265            PdEventData::Connect => Some((&e.timestamp, true)),
266            PdEventData::Disconnect => Some((&e.timestamp, false)),
267            _ => None,
268        })
269    }
270}
271
272#[cfg(feature = "python")]
273#[pyo3::pymethods]
274impl PdEvent {
275    #[getter]
276    fn timestamp(&self) -> f64 {
277        self.timestamp.get::<millisecond>()
278    }
279
280    #[getter]
281    fn data(&self) -> PdEventData {
282        self.data.clone()
283    }
284
285    fn __repr__(&self) -> String {
286        match &self.data {
287            PdEventData::Connect => format!(
288                "PdEvent(timestamp={}ms, type=connect)",
289                self.timestamp.get::<millisecond>()
290            ),
291            PdEventData::Disconnect => format!(
292                "PdEvent(timestamp={}ms, type=disconnect)",
293                self.timestamp.get::<millisecond>()
294            ),
295            PdEventData::PdMessage { sop, wire_data } => format!(
296                "PdEvent(timestamp={}, type=pd_message, sop={}, {} bytes)",
297                self.timestamp.get::<millisecond>(),
298                sop,
299                wire_data.len()
300            ),
301        }
302    }
303
304    fn __str__(&self) -> String {
305        self.__repr__()
306    }
307}
308
309#[cfg(feature = "python")]
310#[pyo3::pymethods]
311impl PdEventStream {
312    #[getter]
313    fn preamble(&self) -> PdStatus {
314        self.preamble
315    }
316
317    #[getter]
318    fn events(&self) -> Vec<PdEvent> {
319        self.events.clone()
320    }
321
322    fn __repr__(&self) -> String {
323        format!(
324            "PdEventStream(preamble={}, {} events)",
325            self.preamble.__repr__(),
326            self.events.len()
327        )
328    }
329
330    fn __str__(&self) -> String {
331        self.__repr__()
332    }
333}