1use pyo3::prelude::*;
8use pyo3::types::{PyBytes, PyDict, PyList};
9
10use spvirit_client::put_encode::encode_put_payload as client_encode_put_payload;
11use spvirit_codec::epics_decode::PvaPacket;
12use spvirit_codec::spvd_decode::{
13 DecodedValue, FieldDesc, FieldType, PvdDecoder, StructureDesc, TypeCode,
14 extract_nt_scalar_value, format_compact_value,
15};
16use spvirit_codec::spvd_encode::encode_pv_request as codec_encode_pv_request;
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]
46 fn name(&self) -> &str {
47 &self.inner.name
48 }
49
50 #[getter]
52 fn field_type(&self) -> &'static str {
53 self.inner.field_type.type_name()
54 }
55
56 #[getter]
59 fn type_code(&self) -> Option<&'static str> {
60 match &self.inner.field_type {
61 FieldType::Scalar(tc) | FieldType::ScalarArray(tc) => Some(type_code_name(*tc)),
62 FieldType::String | FieldType::StringArray | FieldType::BoundedString(_) => {
63 Some("string")
64 }
65 _ => None,
66 }
67 }
68
69 #[getter]
71 fn is_array(&self) -> bool {
72 matches!(
73 self.inner.field_type,
74 FieldType::ScalarArray(_)
75 | FieldType::StringArray
76 | FieldType::StructureArray(_)
77 | FieldType::UnionArray(_)
78 | FieldType::VariantArray
79 )
80 }
81
82 #[getter]
85 fn struct_desc(&self) -> Option<PyStructureDesc> {
86 match &self.inner.field_type {
87 FieldType::Structure(s) | FieldType::StructureArray(s) => {
88 Some(PyStructureDesc::from_inner(s.clone()))
89 }
90 _ => None,
91 }
92 }
93
94 fn __repr__(&self) -> String {
95 format!(
96 "FieldDesc(name={:?}, field_type={:?})",
97 self.inner.name,
98 self.inner.field_type.type_name()
99 )
100 }
101}
102
103fn type_code_name(tc: TypeCode) -> &'static str {
104 match tc {
105 TypeCode::Null => "null",
106 TypeCode::Boolean => "boolean",
107 TypeCode::Int8 => "int8",
108 TypeCode::Int16 => "int16",
109 TypeCode::Int32 => "int32",
110 TypeCode::Int64 => "int64",
111 TypeCode::UInt8 => "uint8",
112 TypeCode::UInt16 => "uint16",
113 TypeCode::UInt32 => "uint32",
114 TypeCode::UInt64 => "uint64",
115 TypeCode::Float32 => "float32",
116 TypeCode::Float64 => "float64",
117 TypeCode::String => "string",
118 TypeCode::Variant => "any",
119 }
120}
121
122#[pyclass(name = "StructureDesc", frozen, module = "spvirit.codec")]
124#[derive(Clone)]
125pub struct PyStructureDesc {
126 inner: StructureDesc,
127}
128
129impl PyStructureDesc {
130 pub(crate) fn from_inner(inner: StructureDesc) -> Self {
131 Self { inner }
132 }
133
134 pub(crate) fn inner(&self) -> &StructureDesc {
135 &self.inner
136 }
137}
138
139#[pymethods]
140impl PyStructureDesc {
141 #[getter]
143 fn struct_id(&self) -> Option<&str> {
144 self.inner.struct_id.as_deref()
145 }
146
147 #[getter]
149 fn fields(&self) -> Vec<PyFieldDesc> {
150 self.inner
151 .fields
152 .iter()
153 .cloned()
154 .map(PyFieldDesc::from_inner)
155 .collect()
156 }
157
158 fn field(&self, name: &str) -> Option<PyFieldDesc> {
160 self.inner.field(name).cloned().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 =
249 client_encode_put_payload(desc.inner(), &json, is_be).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("payload", PyBytes::new(py, &data[8.min(data.len())..]))?;
303
304 let details = PyDict::new(py);
305 if let Some(cmd) = pkt.decode_payload() {
306 fill_command_details(py, &cmd, &details)?;
307 }
308 out.set_item("details", details)?;
309 Ok(out)
310}
311
312pub(crate) fn fill_command_details(
313 py: Python<'_>,
314 cmd: &spvirit_codec::epics_decode::PvaPacketCommand,
315 details: &Bound<'_, PyDict>,
316) -> PyResult<()> {
317 use spvirit_codec::epics_decode::PvaPacketCommand as C;
318
319 match cmd {
320 C::Control(p) => {
321 details.set_item("kind", "control")?;
322 details.set_item("command", p.command)?;
323 details.set_item("data", p.data)?;
324 }
325 C::Search(_) => {
326 details.set_item("kind", "search")?;
327 }
328 C::SearchResponse(p) => {
329 details.set_item("kind", "search_response")?;
330 details.set_item("guid", PyBytes::new(py, &p.guid))?;
331 details.set_item("seq", p.seq)?;
332 details.set_item("port", p.port)?;
333 details.set_item("protocol", &p.protocol)?;
334 details.set_item("found", p.found)?;
335 details.set_item("cids", PyList::new(py, p.cids.iter().cloned())?)?;
336 }
337 C::Beacon(_) => {
338 details.set_item("kind", "beacon")?;
339 }
340 C::ConnectionValidation(_) => {
341 details.set_item("kind", "connection_validation")?;
342 }
343 C::ConnectionValidated(_) => {
344 details.set_item("kind", "connection_validated")?;
345 }
346 C::AuthNZ(_) => {
347 details.set_item("kind", "authnz")?;
348 }
349 C::AclChange(_) => {
350 details.set_item("kind", "acl_change")?;
351 }
352 C::Op(p) => {
353 details.set_item("kind", "op")?;
354 details.set_item("command", p.command)?;
355 details.set_item("sid_or_cid", p.sid_or_cid)?;
356 details.set_item("ioid", p.ioid)?;
357 details.set_item("subcmd", p.subcmd)?;
358 details.set_item("is_server", p.is_server)?;
359 if let Some(status) = &p.status {
360 let s = PyDict::new(py);
361 s.set_item("code", status.code)?;
362 s.set_item("message", status.message.as_deref())?;
363 s.set_item("is_error", status.is_error())?;
364 details.set_item("status", s)?;
365 }
366 if let Some(intro) = &p.introspection {
367 details.set_item("introspection", PyStructureDesc::from_inner(intro.clone()))?;
368 }
369 }
370 C::CreateChannel(p) => {
371 details.set_item("kind", "create_channel")?;
372 details.set_item("is_server", p.is_server)?;
373 details.set_item("cid", p.cid)?;
374 details.set_item("sid", p.sid)?;
375 if !p.channels.is_empty() {
376 let pairs: Vec<(u32, String)> = p.channels.clone();
377 details.set_item("channels", pairs)?;
378 }
379 if let Some(status) = &p.status {
380 let s = PyDict::new(py);
381 s.set_item("code", status.code)?;
382 s.set_item("message", status.message.as_deref())?;
383 s.set_item("is_error", status.is_error())?;
384 details.set_item("status", s)?;
385 }
386 }
387 C::DestroyChannel(p) => {
388 details.set_item("kind", "destroy_channel")?;
389 details.set_item("sid", p.sid)?;
390 details.set_item("cid", p.cid)?;
391 }
392 C::GetField(p) => {
393 details.set_item("kind", "get_field")?;
394 details.set_item("is_server", p.is_server)?;
395 details.set_item("sid", p.sid)?;
396 details.set_item("ioid", p.ioid)?;
397 details.set_item("field_name", p.field_name.as_deref())?;
398 }
399 C::Message(_) => {
400 details.set_item("kind", "message")?;
401 }
402 C::MultipleData(_) => {
403 details.set_item("kind", "multiple_data")?;
404 }
405 C::CancelRequest(_) => {
406 details.set_item("kind", "cancel_request")?;
407 }
408 C::DestroyRequest(_) => {
409 details.set_item("kind", "destroy_request")?;
410 }
411 C::OriginTag(_) => {
412 details.set_item("kind", "origin_tag")?;
413 }
414 C::Echo(data) => {
415 details.set_item("kind", "echo")?;
416 details.set_item("data", PyBytes::new(py, data))?;
417 }
418 C::Unknown(_) => {
419 details.set_item("kind", "unknown")?;
420 }
421 }
422 Ok(())
423}
424
425fn py_to_decoded(obj: &Bound<'_, PyAny>) -> PyResult<DecodedValue> {
429 use pyo3::types::{PyBool, PyFloat, PyInt, PyString};
430
431 if obj.is_none() {
432 return Ok(DecodedValue::Null);
433 }
434 if let Ok(b) = obj.downcast::<PyBool>() {
435 return Ok(DecodedValue::Boolean(b.is_true()));
436 }
437 if obj.is_instance_of::<PyInt>() {
438 let v: i64 = obj.extract()?;
439 return Ok(DecodedValue::Int64(v));
440 }
441 if obj.is_instance_of::<PyFloat>() {
442 let v: f64 = obj.extract()?;
443 return Ok(DecodedValue::Float64(v));
444 }
445 if obj.is_instance_of::<PyString>() {
446 let v: String = obj.extract()?;
447 return Ok(DecodedValue::String(v));
448 }
449 if let Ok(b) = obj.downcast::<pyo3::types::PyBytes>() {
450 return Ok(DecodedValue::Raw(b.as_bytes().to_vec()));
451 }
452 if let Ok(list) = obj.downcast::<PyList>() {
453 let mut out = Vec::with_capacity(list.len());
454 for item in list.iter() {
455 out.push(py_to_decoded(&item)?);
456 }
457 return Ok(DecodedValue::Array(out));
458 }
459 if let Ok(dict) = obj.downcast::<PyDict>() {
460 let mut out = Vec::new();
461 for (k, v) in dict.iter() {
462 let key: String = k.extract()?;
463 out.push((key, py_to_decoded(&v)?));
464 }
465 return Ok(DecodedValue::Structure(out));
466 }
467 Err(decode_msg_to_py_err(format!(
468 "cannot convert {} to DecodedValue",
469 obj.get_type().name()?
470 )))
471}
472
473pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
475 let py = parent.py();
476 let m = PyModule::new(py, "codec")?;
477 m.add_class::<PyFieldDesc>()?;
478 m.add_class::<PyStructureDesc>()?;
479 m.add_function(wrap_pyfunction!(decode_introspection, &m)?)?;
480 m.add_function(wrap_pyfunction!(decode_value, &m)?)?;
481 m.add_function(wrap_pyfunction!(encode_pv_request, &m)?)?;
482 m.add_function(wrap_pyfunction!(encode_put_payload, &m)?)?;
483 m.add_function(wrap_pyfunction!(format_value, &m)?)?;
484 m.add_function(wrap_pyfunction!(extract_nt_value, &m)?)?;
485 m.add_function(wrap_pyfunction!(decode_packet, &m)?)?;
486 parent.add_submodule(&m)?;
487 py.import("sys")?
489 .getattr("modules")?
490 .set_item("spvirit.codec", &m)?;
491 Ok(())
492}