Skip to main content

spvirit/
packet.rs

1//! Raw-frame `Packet` wrapper exposed via `Channel.read_packet` /
2//! `Channel.read_until`.
3
4use pyo3::prelude::*;
5use pyo3::types::{PyBytes, PyDict};
6
7use spvirit_codec::epics_decode::{PvaPacket, command_name};
8
9/// Owned PVA frame with header accessors and on-demand detail decoding.
10#[pyclass(name = "Packet", module = "spvirit.lowlevel", frozen)]
11pub struct PyPacket {
12    bytes: Vec<u8>,
13    magic: u8,
14    version: u8,
15    command: u8,
16    flags_raw: u8,
17    is_application: bool,
18    is_control: bool,
19    is_segmented: u8,
20    is_client: bool,
21    is_server: bool,
22    is_msb: bool,
23    payload_length: u32,
24}
25
26impl PyPacket {
27    pub(crate) fn raw(&self) -> &[u8] {
28        &self.bytes
29    }
30
31    pub fn from_bytes(bytes: Vec<u8>) -> Self {
32        if bytes.len() >= 8 {
33            let pkt = PvaPacket::new(&bytes);
34            return Self {
35                magic: pkt.header.magic,
36                version: pkt.header.version,
37                command: pkt.header.command,
38                flags_raw: pkt.header.flags.raw,
39                is_application: pkt.header.flags.is_application,
40                is_control: pkt.header.flags.is_control,
41                is_segmented: pkt.header.flags.is_segmented,
42                is_client: pkt.header.flags.is_client,
43                is_server: pkt.header.flags.is_server,
44                is_msb: pkt.header.flags.is_msb,
45                payload_length: pkt.header.payload_length,
46                bytes,
47            };
48        }
49        Self {
50            magic: 0,
51            version: 0,
52            command: 0,
53            flags_raw: 0,
54            is_application: false,
55            is_control: false,
56            is_segmented: 0,
57            is_client: false,
58            is_server: false,
59            is_msb: false,
60            payload_length: 0,
61            bytes,
62        }
63    }
64}
65
66#[pymethods]
67impl PyPacket {
68    /// Header magic byte (0xCA for a valid PVA frame).
69    #[getter]
70    fn magic(&self) -> u8 {
71        self.magic
72    }
73    /// Protocol version byte from the header.
74    #[getter]
75    fn version(&self) -> u8 {
76        self.version
77    }
78    /// Command byte from the header.
79    #[getter]
80    fn command(&self) -> u8 {
81        self.command
82    }
83    /// Human-readable name of the command byte.
84    #[getter]
85    fn command_name(&self) -> &'static str {
86        command_name(self.command)
87    }
88    /// Raw header flags byte.
89    #[getter]
90    fn flags(&self) -> u8 {
91        self.flags_raw
92    }
93    /// True if this is an application message.
94    #[getter]
95    fn is_application(&self) -> bool {
96        self.is_application
97    }
98    /// True if this is a control message.
99    #[getter]
100    fn is_control(&self) -> bool {
101        self.is_control
102    }
103    /// Segmentation flag bits as an int; 0 means unsegmented.
104    #[getter]
105    fn is_segmented(&self) -> u8 {
106        self.is_segmented
107    }
108    /// True if the message was sent by a client.
109    #[getter]
110    fn is_client(&self) -> bool {
111        self.is_client
112    }
113    /// True if the message was sent by a server.
114    #[getter]
115    fn is_server(&self) -> bool {
116        self.is_server
117    }
118    /// True if the payload is big-endian (MSB flag set).
119    #[getter]
120    fn is_msb(&self) -> bool {
121        self.is_msb
122    }
123    /// Payload length in bytes as declared in the header.
124    #[getter]
125    fn payload_length(&self) -> u32 {
126        self.payload_length
127    }
128
129    /// Complete frame bytes (header + payload).
130    #[getter]
131    fn bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
132        PyBytes::new(py, &self.bytes)
133    }
134
135    /// Payload bytes (everything after the 8-byte header).
136    #[getter]
137    fn payload<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
138        let start = 8.min(self.bytes.len());
139        PyBytes::new(py, &self.bytes[start..])
140    }
141
142    fn __len__(&self) -> usize {
143        self.bytes.len()
144    }
145
146    /// Decode command-specific payload details.  Delegates to the
147    /// standalone codec module.
148    fn details<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
149        let mut pkt = PvaPacket::new(&self.bytes);
150        let d = PyDict::new(py);
151        if let Some(cmd) = pkt.decode_payload() {
152            crate::codec::fill_command_details(py, &cmd, &d)?;
153        }
154        Ok(d)
155    }
156
157    /// Alias for the free `spvirit.codec.decode_packet` function
158    /// applied to this packet's bytes.
159    fn decode<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
160        crate::codec::decode_packet(py, &self.bytes)
161    }
162
163    fn __repr__(&self) -> String {
164        format!(
165            "Packet(command={} ({}), flags=0x{:02x}, payload_length={})",
166            self.command,
167            self.command_name(),
168            self.flags_raw,
169            self.payload_length
170        )
171    }
172}