1use pyo3::prelude::*;
8use pyo3::types::{PyBytes, PyDict, PyList};
9
10use spvirit_codec::epics_decode::PvaPacket;
11use spvirit_codec::spvd_decode::{
12 DecodedValue, FieldDesc, FieldType, PvdDecoder, StructureDesc, TypeCode,
13 extract_nt_scalar_value, format_compact_value,
14};
15use spvirit_codec::spvd_encode::encode_pv_request as codec_encode_pv_request;
16use spvirit_client::put_encode::encode_put_payload as client_encode_put_payload;
17
18use crate::convert::{decoded_to_py, py_to_json};
19use crate::errors::{decode_msg_to_py_err, protocol_msg_to_py_err};
20
21#[pyclass(name = "FieldDesc", frozen, module = "spvirit.codec")]
26#[derive(Clone)]
27pub struct PyFieldDesc {
28 inner: FieldDesc,
29}
30
31impl PyFieldDesc {
32 pub(crate) fn from_inner(inner: FieldDesc) -> Self {
33 Self { inner }
34 }
35
36 #[allow(dead_code)]
37 pub(crate) fn inner(&self) -> &FieldDesc {
38 &self.inner
39 }
40}
41
42#[pymethods]
43impl PyFieldDesc {
44 #[getter]
45 fn name(&self) -> &str {
46 &self.inner.name
47 }
48
49 #[getter]
51 fn field_type(&self) -> &'static str {
52 self.inner.field_type.type_name()
53 }
54
55 #[getter]
58 fn type_code(&self) -> Option<&'static str> {
59 match &self.inner.field_type {
60 FieldType::Scalar(tc) | FieldType::ScalarArray(tc) => Some(type_code_name(*tc)),
61 FieldType::String | FieldType::StringArray | FieldType::BoundedString(_) => {
62 Some("string")
63 }
64 _ => None,
65 }
66 }
67
68 #[getter]
70 fn is_array(&self) -> bool {
71 matches!(
72 self.inner.field_type,
73 FieldType::ScalarArray(_)
74 | FieldType::StringArray
75 | FieldType::StructureArray(_)
76 | FieldType::UnionArray(_)
77 | FieldType::VariantArray
78 )
79 }
80
81 #[getter]
84 fn struct_desc(&self) -> Option<PyStructureDesc> {
85 match &self.inner.field_type {
86 FieldType::Structure(s) | FieldType::StructureArray(s) => {
87 Some(PyStructureDesc::from_inner(s.clone()))
88 }
89 _ => None,
90 }
91 }
92
93 fn __repr__(&self) -> String {
94 format!(
95 "FieldDesc(name={:?}, field_type={:?})",
96 self.inner.name,
97 self.inner.field_type.type_name()
98 )
99 }
100}
101
102fn type_code_name(tc: TypeCode) -> &'static str {
103 match tc {
104 TypeCode::Null => "null",
105 TypeCode::Boolean => "boolean",
106 TypeCode::Int8 => "int8",
107 TypeCode::Int16 => "int16",
108 TypeCode::Int32 => "int32",
109 TypeCode::Int64 => "int64",
110 TypeCode::UInt8 => "uint8",
111 TypeCode::UInt16 => "uint16",
112 TypeCode::UInt32 => "uint32",
113 TypeCode::UInt64 => "uint64",
114 TypeCode::Float32 => "float32",
115 TypeCode::Float64 => "float64",
116 TypeCode::String => "string",
117 TypeCode::Variant => "any",
118 }
119}
120
121#[pyclass(name = "StructureDesc", frozen, module = "spvirit.codec")]
123#[derive(Clone)]
124pub struct PyStructureDesc {
125 inner: StructureDesc,
126}
127
128impl PyStructureDesc {
129 pub(crate) fn from_inner(inner: StructureDesc) -> Self {
130 Self { inner }
131 }
132
133 pub(crate) fn inner(&self) -> &StructureDesc {
134 &self.inner
135 }
136}
137
138#[pymethods]
139impl PyStructureDesc {
140 #[getter]
141 fn struct_id(&self) -> Option<&str> {
142 self.inner.struct_id.as_deref()
143 }
144
145 #[getter]
146 fn fields(&self) -> Vec<PyFieldDesc> {
147 self.inner
148 .fields
149 .iter()
150 .cloned()
151 .map(PyFieldDesc::from_inner)
152 .collect()
153 }
154
155 fn field(&self, name: &str) -> Option<PyFieldDesc> {
157 self.inner
158 .field(name)
159 .cloned()
160 .map(PyFieldDesc::from_inner)
161 }
162
163 fn __len__(&self) -> usize {
164 self.inner.fields.len()
165 }
166
167 fn __contains__(&self, name: &str) -> bool {
168 self.inner.field(name).is_some()
169 }
170
171 fn __repr__(&self) -> String {
172 format!(
173 "StructureDesc(struct_id={:?}, fields={})",
174 self.inner.struct_id,
175 self.inner.fields.len()
176 )
177 }
178
179 fn dump(&self) -> String {
181 format!("{}", self.inner)
182 }
183}
184
185#[pyfunction]
190#[pyo3(signature = (data, is_be=false))]
191pub fn decode_introspection(data: &[u8], is_be: bool) -> PyResult<PyStructureDesc> {
192 let decoder = PvdDecoder::new(is_be);
193 decoder
194 .parse_introspection(data)
195 .map(PyStructureDesc::from_inner)
196 .ok_or_else(|| decode_msg_to_py_err("failed to parse introspection"))
197}
198
199#[pyfunction]
201#[pyo3(signature = (data, desc, is_be=false))]
202pub fn decode_value(
203 py: Python<'_>,
204 data: &[u8],
205 desc: &PyStructureDesc,
206 is_be: bool,
207) -> PyResult<PyObject> {
208 let decoder = PvdDecoder::new(is_be);
209 let ft = FieldType::Structure(desc.inner().clone());
211 let (decoded, _consumed) = decoder
212 .decode_value(data, &ft)
213 .ok_or_else(|| decode_msg_to_py_err("failed to decode value"))?;
214 Ok(decoded_to_py(py, &decoded))
215}
216
217#[pyfunction]
220#[pyo3(signature = (fields=None, is_be=false))]
221pub fn encode_pv_request(
222 py: Python<'_>,
223 fields: Option<Vec<String>>,
224 is_be: bool,
225) -> PyResult<PyObject> {
226 let fields = fields.unwrap_or_default();
227 let bytes = if fields.is_empty() {
228 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
229 } else {
230 let refs: Vec<&str> = fields.iter().map(String::as_str).collect();
231 codec_encode_pv_request(&refs, is_be)
232 };
233 Ok(PyBytes::new(py, &bytes).into_any().unbind())
234}
235
236#[pyfunction]
240#[pyo3(signature = (desc, value, is_be=false))]
241pub fn encode_put_payload(
242 py: Python<'_>,
243 desc: &PyStructureDesc,
244 value: PyObject,
245 is_be: bool,
246) -> PyResult<PyObject> {
247 let json = py_to_json(value.bind(py))?;
248 let bytes = client_encode_put_payload(desc.inner(), &json, is_be)
249 .map_err(protocol_msg_to_py_err)?;
250 Ok(PyBytes::new(py, &bytes).into_any().unbind())
251}
252
253#[pyfunction]
258pub fn format_value(py: Python<'_>, value: PyObject) -> PyResult<String> {
259 let bound = value.bind(py);
260 let decoded = py_to_decoded(bound)?;
261 Ok(format_compact_value(&decoded))
262}
263
264#[pyfunction]
267pub fn extract_nt_value(py: Python<'_>, value: PyObject) -> PyResult<Option<PyObject>> {
268 let bound = value.bind(py);
269 let decoded = py_to_decoded(bound)?;
270 Ok(extract_nt_scalar_value(&decoded).map(|v| decoded_to_py(py, v)))
271}
272
273#[pyfunction]
277pub fn decode_packet<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyDict>> {
278 use spvirit_codec::epics_decode::command_name;
279
280 if data.len() < 8 {
281 return Err(decode_msg_to_py_err("packet shorter than 8-byte header"));
282 }
283
284 let mut pkt = PvaPacket::new(data);
285 let out = PyDict::new(py);
286 out.set_item("magic", pkt.header.magic)?;
287 out.set_item("version", pkt.header.version)?;
288 out.set_item("command", pkt.header.command)?;
289 out.set_item("command_name", command_name(pkt.header.command))?;
290 out.set_item("payload_length", pkt.header.payload_length)?;
291
292 let flags = PyDict::new(py);
293 flags.set_item("raw", pkt.header.flags.raw)?;
294 flags.set_item("is_application", pkt.header.flags.is_application)?;
295 flags.set_item("is_control", pkt.header.flags.is_control)?;
296 flags.set_item("is_segmented", pkt.header.flags.is_segmented)?;
297 flags.set_item("is_client", pkt.header.flags.is_client)?;
298 flags.set_item("is_server", pkt.header.flags.is_server)?;
299 flags.set_item("is_msb", pkt.header.flags.is_msb)?;
300 out.set_item("flags", flags)?;
301
302 out.set_item(
303 "payload",
304 PyBytes::new(py, &data[8.min(data.len())..]),
305 )?;
306
307 let details = PyDict::new(py);
308 if let Some(cmd) = pkt.decode_payload() {
309 fill_command_details(py, &cmd, &details)?;
310 }
311 out.set_item("details", details)?;
312 Ok(out)
313}
314
315pub(crate) fn fill_command_details(
316 py: Python<'_>,
317 cmd: &spvirit_codec::epics_decode::PvaPacketCommand,
318 details: &Bound<'_, PyDict>,
319) -> PyResult<()> {
320 use spvirit_codec::epics_decode::PvaPacketCommand as C;
321
322 match cmd {
323 C::Control(p) => {
324 details.set_item("kind", "control")?;
325 details.set_item("command", p.command)?;
326 details.set_item("data", p.data)?;
327 }
328 C::Search(_) => {
329 details.set_item("kind", "search")?;
330 }
331 C::SearchResponse(p) => {
332 details.set_item("kind", "search_response")?;
333 details.set_item("guid", PyBytes::new(py, &p.guid))?;
334 details.set_item("seq", p.seq)?;
335 details.set_item("port", p.port)?;
336 details.set_item("protocol", &p.protocol)?;
337 details.set_item("found", p.found)?;
338 details.set_item("cids", PyList::new(py, p.cids.iter().cloned())?)?;
339 }
340 C::Beacon(_) => {
341 details.set_item("kind", "beacon")?;
342 }
343 C::ConnectionValidation(_) => {
344 details.set_item("kind", "connection_validation")?;
345 }
346 C::ConnectionValidated(_) => {
347 details.set_item("kind", "connection_validated")?;
348 }
349 C::AuthNZ(_) => {
350 details.set_item("kind", "authnz")?;
351 }
352 C::AclChange(_) => {
353 details.set_item("kind", "acl_change")?;
354 }
355 C::Op(p) => {
356 details.set_item("kind", "op")?;
357 details.set_item("command", p.command)?;
358 details.set_item("sid_or_cid", p.sid_or_cid)?;
359 details.set_item("ioid", p.ioid)?;
360 details.set_item("subcmd", p.subcmd)?;
361 details.set_item("is_server", p.is_server)?;
362 if let Some(status) = &p.status {
363 let s = PyDict::new(py);
364 s.set_item("code", status.code)?;
365 s.set_item("message", status.message.as_deref())?;
366 s.set_item("is_error", status.is_error())?;
367 details.set_item("status", s)?;
368 }
369 if let Some(intro) = &p.introspection {
370 details.set_item(
371 "introspection",
372 PyStructureDesc::from_inner(intro.clone()),
373 )?;
374 }
375 }
376 C::CreateChannel(p) => {
377 details.set_item("kind", "create_channel")?;
378 details.set_item("is_server", p.is_server)?;
379 details.set_item("cid", p.cid)?;
380 details.set_item("sid", p.sid)?;
381 if !p.channels.is_empty() {
382 let pairs: Vec<(u32, String)> = p.channels.clone();
383 details.set_item("channels", pairs)?;
384 }
385 if let Some(status) = &p.status {
386 let s = PyDict::new(py);
387 s.set_item("code", status.code)?;
388 s.set_item("message", status.message.as_deref())?;
389 s.set_item("is_error", status.is_error())?;
390 details.set_item("status", s)?;
391 }
392 }
393 C::DestroyChannel(p) => {
394 details.set_item("kind", "destroy_channel")?;
395 details.set_item("sid", p.sid)?;
396 details.set_item("cid", p.cid)?;
397 }
398 C::GetField(p) => {
399 details.set_item("kind", "get_field")?;
400 details.set_item("is_server", p.is_server)?;
401 details.set_item("sid", p.sid)?;
402 details.set_item("ioid", p.ioid)?;
403 details.set_item("field_name", p.field_name.as_deref())?;
404 }
405 C::Message(_) => {
406 details.set_item("kind", "message")?;
407 }
408 C::MultipleData(_) => {
409 details.set_item("kind", "multiple_data")?;
410 }
411 C::CancelRequest(_) => {
412 details.set_item("kind", "cancel_request")?;
413 }
414 C::DestroyRequest(_) => {
415 details.set_item("kind", "destroy_request")?;
416 }
417 C::OriginTag(_) => {
418 details.set_item("kind", "origin_tag")?;
419 }
420 C::Echo(data) => {
421 details.set_item("kind", "echo")?;
422 details.set_item("data", PyBytes::new(py, data))?;
423 }
424 C::Unknown(_) => {
425 details.set_item("kind", "unknown")?;
426 }
427 }
428 Ok(())
429}
430
431fn py_to_decoded(obj: &Bound<'_, PyAny>) -> PyResult<DecodedValue> {
435 use pyo3::types::{PyBool, PyFloat, PyInt, PyString};
436
437 if obj.is_none() {
438 return Ok(DecodedValue::Null);
439 }
440 if let Ok(b) = obj.downcast::<PyBool>() {
441 return Ok(DecodedValue::Boolean(b.is_true()));
442 }
443 if obj.is_instance_of::<PyInt>() {
444 let v: i64 = obj.extract()?;
445 return Ok(DecodedValue::Int64(v));
446 }
447 if obj.is_instance_of::<PyFloat>() {
448 let v: f64 = obj.extract()?;
449 return Ok(DecodedValue::Float64(v));
450 }
451 if obj.is_instance_of::<PyString>() {
452 let v: String = obj.extract()?;
453 return Ok(DecodedValue::String(v));
454 }
455 if let Ok(b) = obj.downcast::<pyo3::types::PyBytes>() {
456 return Ok(DecodedValue::Raw(b.as_bytes().to_vec()));
457 }
458 if let Ok(list) = obj.downcast::<PyList>() {
459 let mut out = Vec::with_capacity(list.len());
460 for item in list.iter() {
461 out.push(py_to_decoded(&item)?);
462 }
463 return Ok(DecodedValue::Array(out));
464 }
465 if let Ok(dict) = obj.downcast::<PyDict>() {
466 let mut out = Vec::new();
467 for (k, v) in dict.iter() {
468 let key: String = k.extract()?;
469 out.push((key, py_to_decoded(&v)?));
470 }
471 return Ok(DecodedValue::Structure(out));
472 }
473 Err(decode_msg_to_py_err(format!(
474 "cannot convert {} to DecodedValue",
475 obj.get_type().name()?
476 )))
477}
478
479pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
481 let py = parent.py();
482 let m = PyModule::new(py, "codec")?;
483 m.add_class::<PyFieldDesc>()?;
484 m.add_class::<PyStructureDesc>()?;
485 m.add_function(wrap_pyfunction!(decode_introspection, &m)?)?;
486 m.add_function(wrap_pyfunction!(decode_value, &m)?)?;
487 m.add_function(wrap_pyfunction!(encode_pv_request, &m)?)?;
488 m.add_function(wrap_pyfunction!(encode_put_payload, &m)?)?;
489 m.add_function(wrap_pyfunction!(format_value, &m)?)?;
490 m.add_function(wrap_pyfunction!(extract_nt_value, &m)?)?;
491 m.add_function(wrap_pyfunction!(decode_packet, &m)?)?;
492 parent.add_submodule(&m)?;
493 py.import("sys")?
495 .getattr("modules")?
496 .set_item("spvirit.codec", &m)?;
497 Ok(())
498}