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 Status block (12 bytes) - appears in ADC+PD packets
19#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
20#[repr(C)]
21pub struct PdStatusRaw {
22    pub type_id: u8,
23    pub timestamp24: [u8; 3], // 24-bit little-endian
24    pub vbus_mv: U16,
25    pub ibus_ma: I16, // Signed! Negative = power flowing from male to female port
26    pub cc1_mv: U16,
27    pub cc2_mv: U16,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object))]
33pub struct PdStatus {
34    pub type_id: u8,
35    /// Coarse protocol counter, approximately 40 ms per tick.
36    pub timestamp_ticks: u32,
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        // Convert 24-bit timestamp to 32-bit
46        let timestamp = u32::from_le_bytes([raw.timestamp24[0], raw.timestamp24[1], raw.timestamp24[2], 0]);
47
48        Self {
49            type_id: raw.type_id,
50            timestamp_ticks: timestamp,
51            vbus: ElectricPotential::new::<millivolt>(f64::from(raw.vbus_mv.get())),
52            ibus: ElectricCurrent::new::<milliampere>(f64::from(raw.ibus_ma.get())),
53            cc1: ElectricPotential::new::<millivolt>(f64::from(raw.cc1_mv.get())),
54            cc2: ElectricPotential::new::<millivolt>(f64::from(raw.cc2_mv.get())),
55        }
56    }
57}
58
59#[cfg(feature = "python")]
60#[pyo3::pymethods]
61impl PdStatus {
62    #[getter]
63    fn type_id(&self) -> u8 {
64        self.type_id
65    }
66    #[getter]
67    fn timestamp(&self) -> u32 {
68        self.timestamp_ticks
69    }
70    #[getter]
71    fn vbus_v(&self) -> f64 {
72        self.vbus.get::<volt>()
73    }
74    #[getter]
75    fn ibus_a(&self) -> f64 {
76        self.ibus.get::<ampere>()
77    }
78    #[getter]
79    fn cc1_v(&self) -> f64 {
80        self.cc1.get::<volt>()
81    }
82    #[getter]
83    fn cc2_v(&self) -> f64 {
84        self.cc2.get::<volt>()
85    }
86
87    fn __repr__(&self) -> String {
88        format!(
89            "PdStatus(type_id={}, timestamp={}, vbus={:.3}V, ibus={:.3}A)",
90            self.type_id,
91            self.timestamp_ticks,
92            self.vbus.get::<volt>(),
93            self.ibus.get::<ampere>()
94        )
95    }
96
97    fn __str__(&self) -> String {
98        self.__repr__()
99    }
100}
101
102/// PD Preamble (12 bytes) - appears before event stream in PD-only responses
103#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
104#[repr(C)]
105pub struct PdPreambleRaw {
106    pub timestamp32: U32,
107    pub vbus_mv: U16,
108    pub ibus_ma: I16, // Signed!
109    pub cc1_mv: U16,
110    pub cc2_mv: U16,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
114#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
115#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object))]
116pub struct PdPreamble {
117    pub timestamp: Time,
118    pub vbus: ElectricPotential,
119    pub ibus: ElectricCurrent,
120    pub cc1: ElectricPotential,
121    pub cc2: ElectricPotential,
122}
123
124impl From<PdPreambleRaw> for PdPreamble {
125    fn from(raw: PdPreambleRaw) -> Self {
126        Self {
127            timestamp: Time::new::<millisecond>(f64::from(raw.timestamp32.get())),
128            vbus: ElectricPotential::new::<millivolt>(f64::from(raw.vbus_mv.get())),
129            ibus: ElectricCurrent::new::<milliampere>(f64::from(raw.ibus_ma.get())),
130            cc1: ElectricPotential::new::<millivolt>(f64::from(raw.cc1_mv.get())),
131            cc2: ElectricPotential::new::<millivolt>(f64::from(raw.cc2_mv.get())),
132        }
133    }
134}
135
136#[cfg(feature = "python")]
137#[pyo3::pymethods]
138impl PdPreamble {
139    #[getter]
140    fn timestamp(&self) -> f64 {
141        self.timestamp.get::<millisecond>()
142    }
143    #[getter]
144    fn vbus_v(&self) -> f64 {
145        self.vbus.get::<volt>()
146    }
147    #[getter]
148    fn ibus_a(&self) -> f64 {
149        self.ibus.get::<ampere>()
150    }
151    #[getter]
152    fn cc1_v(&self) -> f64 {
153        self.cc1.get::<volt>()
154    }
155    #[getter]
156    fn cc2_v(&self) -> f64 {
157        self.cc2.get::<volt>()
158    }
159
160    fn __repr__(&self) -> String {
161        format!(
162            "PdPreamble(timestamp={}ms, vbus={:.3}V, ibus={:.3}A)",
163            self.timestamp.get::<millisecond>(),
164            self.vbus.get::<volt>(),
165            self.ibus.get::<ampere>()
166        )
167    }
168
169    fn __str__(&self) -> String {
170        self.__repr__()
171    }
172}
173
174/// Event data types that can appear in PD stream
175#[derive(Debug, Clone, PartialEq)]
176#[cfg_attr(feature = "python", derive(pyo3::IntoPyObject))]
177pub enum PdEventData {
178    #[cfg_attr(feature = "python", pyo3(transparent))]
179    Connect(()),
180    #[cfg_attr(feature = "python", pyo3(transparent))]
181    Disconnect(()),
182    PdMessage {
183        sop: u8,
184        wire_data: Vec<u8>,
185    },
186}
187
188/// Timestamped PD event
189#[derive(Debug, Clone, PartialEq)]
190#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "PdEvent"))]
191pub struct PdEvent {
192    pub timestamp: Time,
193    pub data: PdEventData,
194}
195
196/// Complete PD event stream with preamble and events
197#[derive(Debug, Clone, PartialEq)]
198#[cfg_attr(feature = "python", pyo3::pyclass(skip_from_py_object, name = "PdEventStream"))]
199pub struct PdEventStream {
200    pub preamble: PdPreamble,
201    pub events: Vec<PdEvent>,
202}
203
204impl PdEventStream {
205    /// Parse PD event stream from bytes
206    /// Expected format: 12-byte preamble + repeated (6-byte header + wire data) events
207    pub fn from_bytes(bytes: Bytes) -> Result<Self, KMError> {
208        if bytes.len() < PD_PREAMBLE_SIZE {
209            return Err(KMError::InvalidPacket(format!(
210                "PD event stream too short for preamble: need {}, got {}",
211                PD_PREAMBLE_SIZE,
212                bytes.len()
213            )));
214        }
215
216        // Parse preamble
217        let preamble_raw = PdPreambleRaw::ref_from_bytes(&bytes[..PD_PREAMBLE_SIZE])
218            .map_err(|_| KMError::InvalidPacket("Failed to parse PD preamble".to_string()))?;
219        let preamble = PdPreamble::from(*preamble_raw);
220
221        let mut events = Vec::new();
222        let mut offset = PD_PREAMBLE_SIZE;
223
224        // Parse events
225        while offset < bytes.len() {
226            if bytes.len() - offset < PD_EVENT_HEADER_SIZE {
227                return Err(KMError::InvalidPacket(format!(
228                    "Incomplete PD event header at offset {}: need {}, got {}",
229                    offset,
230                    PD_EVENT_HEADER_SIZE,
231                    bytes.len() - offset
232                )));
233            }
234
235            let size_flag = bytes[offset];
236
237            // Connection/status event (0x45): 6-byte header with ts24 + code
238            if size_flag == PD_EVENT_TYPE_CONNECTION {
239                let ts24 = u32::from_le_bytes([bytes[offset + 1], bytes[offset + 2], bytes[offset + 3], 0]);
240                let event_code = bytes[offset + 5];
241                let data = match event_code {
242                    PD_CONNECTION_CONNECT | PD_CONNECTION_CONNECT_LEGACY => PdEventData::Connect(()),
243                    PD_CONNECTION_DISCONNECT | PD_CONNECTION_DISCONNECT_LEGACY => PdEventData::Disconnect(()),
244                    _ => PdEventData::PdMessage {
245                        sop: event_code,
246                        wire_data: Vec::new(),
247                    },
248                };
249                events.push(PdEvent {
250                    timestamp: Time::new::<millisecond>(f64::from(ts24)),
251                    data,
252                });
253                offset += PD_EVENT_HEADER_SIZE;
254                continue;
255            }
256
257            let timestamp = u32::from_le_bytes([
258                bytes[offset + 1],
259                bytes[offset + 2],
260                bytes[offset + 3],
261                bytes[offset + 4],
262            ]);
263            let sop = bytes[offset + 5];
264
265            // Calculate wire data length: wire_len = (size_flag & mask) - offset
266            let encoded_size = size_flag & PD_EVENT_SIZE_MASK;
267            let wire_len = encoded_size.checked_sub(PD_EVENT_SIZE_OFFSET).ok_or_else(|| {
268                KMError::InvalidPacket(format!(
269                    "Invalid PD event size at offset {}: encoded size {} is below {}",
270                    offset, encoded_size, PD_EVENT_SIZE_OFFSET
271                ))
272            })? as usize;
273
274            offset += PD_EVENT_HEADER_SIZE;
275
276            if bytes.len() - offset < wire_len {
277                return Err(KMError::InvalidPacket(format!(
278                    "Insufficient bytes for wire data: expected {}, got {}",
279                    wire_len,
280                    bytes.len() - offset
281                )));
282            }
283
284            let wire_data = if wire_len > 0 {
285                bytes.slice(offset..offset + wire_len).to_vec()
286            } else {
287                Vec::new()
288            };
289            offset += wire_len;
290
291            let data = PdEventData::PdMessage { sop, wire_data };
292
293            events.push(PdEvent {
294                timestamp: Time::new::<millisecond>(f64::from(timestamp)),
295                data,
296            });
297        }
298
299        Ok(Self { preamble, events })
300    }
301
302    /// Helper: get all PD messages, ignoring connection events
303    pub fn pd_messages(&self) -> impl Iterator<Item = (&Time, u8, &Vec<u8>)> {
304        self.events.iter().filter_map(|e| match &e.data {
305            PdEventData::PdMessage { sop, wire_data } => Some((&e.timestamp, *sop, wire_data)),
306            _ => None,
307        })
308    }
309
310    /// Helper: get connection state changes
311    pub fn connection_events(&self) -> impl Iterator<Item = (&Time, bool)> {
312        self.events.iter().filter_map(|e| match &e.data {
313            PdEventData::Connect(()) => Some((&e.timestamp, true)),
314            PdEventData::Disconnect(()) => Some((&e.timestamp, false)),
315            _ => None,
316        })
317    }
318}
319
320#[cfg(feature = "python")]
321#[pyo3::pymethods]
322impl PdEvent {
323    #[getter]
324    fn timestamp(&self) -> f64 {
325        self.timestamp.get::<millisecond>()
326    }
327
328    #[getter]
329    fn data(&self) -> PdEventData {
330        self.data.clone()
331    }
332
333    fn __repr__(&self) -> String {
334        match &self.data {
335            PdEventData::Connect(()) => format!(
336                "PdEvent(timestamp={}ms, type=connect)",
337                self.timestamp.get::<millisecond>()
338            ),
339            PdEventData::Disconnect(()) => format!(
340                "PdEvent(timestamp={}ms, type=disconnect)",
341                self.timestamp.get::<millisecond>()
342            ),
343            PdEventData::PdMessage { sop, wire_data } => format!(
344                "PdEvent(timestamp={}, type=pd_message, sop={}, {} bytes)",
345                self.timestamp.get::<millisecond>(),
346                sop,
347                wire_data.len()
348            ),
349        }
350    }
351
352    fn __str__(&self) -> String {
353        self.__repr__()
354    }
355}
356
357#[cfg(feature = "python")]
358#[pyo3::pymethods]
359impl PdEventStream {
360    #[getter]
361    fn preamble(&self) -> PdPreamble {
362        self.preamble
363    }
364
365    #[getter]
366    fn events(&self) -> Vec<PdEvent> {
367        self.events.clone()
368    }
369
370    fn __repr__(&self) -> String {
371        format!(
372            "PdEventStream(preamble={}, {} events)",
373            self.preamble.__repr__(),
374            self.events.len()
375        )
376    }
377
378    fn __str__(&self) -> String {
379        self.__repr__()
380    }
381}