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]
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.field(name).cloned().map(PyFieldDesc::from_inner)
158 }
159
160 fn __len__(&self) -> usize {
161 self.inner.fields.len()
162 }
163
164 fn __contains__(&self, name: &str) -> bool {
165 self.inner.field(name).is_some()
166 }
167
168 fn __repr__(&self) -> String {
169 format!(
170 "StructureDesc(struct_id={:?}, fields={})",
171 self.inner.struct_id,
172 self.inner.fields.len()
173 )
174 }
175
176 fn dump(&self) -> String {
178 format!("{}", self.inner)
179 }
180}
181
182#[pyfunction]
187#[pyo3(signature = (data, is_be=false))]
188pub fn decode_introspection(data: &[u8], is_be: bool) -> PyResult<PyStructureDesc> {
189 let decoder = PvdDecoder::new(is_be);
190 decoder
191 .parse_introspection(data)
192 .map(PyStructureDesc::from_inner)
193 .ok_or_else(|| decode_msg_to_py_err("failed to parse introspection"))
194}
195
196#[pyfunction]
198#[pyo3(signature = (data, desc, is_be=false))]
199pub fn decode_value(
200 py: Python<'_>,
201 data: &[u8],
202 desc: &PyStructureDesc,
203 is_be: bool,
204) -> PyResult<PyObject> {
205 let decoder = PvdDecoder::new(is_be);
206 let ft = FieldType::Structure(desc.inner().clone());
208 let (decoded, _consumed) = decoder
209 .decode_value(data, &ft)
210 .ok_or_else(|| decode_msg_to_py_err("failed to decode value"))?;
211 Ok(decoded_to_py(py, &decoded))
212}
213
214#[pyfunction]
217#[pyo3(signature = (fields=None, is_be=false))]
218pub fn encode_pv_request(
219 py: Python<'_>,
220 fields: Option<Vec<String>>,
221 is_be: bool,
222) -> PyResult<PyObject> {
223 let fields = fields.unwrap_or_default();
224 let bytes = if fields.is_empty() {
225 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
226 } else {
227 let refs: Vec<&str> = fields.iter().map(String::as_str).collect();
228 codec_encode_pv_request(&refs, is_be)
229 };
230 Ok(PyBytes::new(py, &bytes).into_any().unbind())
231}
232
233#[pyfunction]
237#[pyo3(signature = (desc, value, is_be=false))]
238pub fn encode_put_payload(
239 py: Python<'_>,
240 desc: &PyStructureDesc,
241 value: PyObject,
242 is_be: bool,
243) -> PyResult<PyObject> {
244 let json = py_to_json(value.bind(py))?;
245 let bytes =
246 client_encode_put_payload(desc.inner(), &json, is_be).map_err(protocol_msg_to_py_err)?;
247 Ok(PyBytes::new(py, &bytes).into_any().unbind())
248}
249
250#[pyfunction]
255pub fn format_value(py: Python<'_>, value: PyObject) -> PyResult<String> {
256 let bound = value.bind(py);
257 let decoded = py_to_decoded(bound)?;
258 Ok(format_compact_value(&decoded))
259}
260
261#[pyfunction]
264pub fn extract_nt_value(py: Python<'_>, value: PyObject) -> PyResult<Option<PyObject>> {
265 let bound = value.bind(py);
266 let decoded = py_to_decoded(bound)?;
267 Ok(extract_nt_scalar_value(&decoded).map(|v| decoded_to_py(py, v)))
268}
269
270#[pyfunction]
274pub fn decode_packet<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyDict>> {
275 use spvirit_codec::epics_decode::command_name;
276
277 if data.len() < 8 {
278 return Err(decode_msg_to_py_err("packet shorter than 8-byte header"));
279 }
280
281 let mut pkt = PvaPacket::new(data);
282 let out = PyDict::new(py);
283 out.set_item("magic", pkt.header.magic)?;
284 out.set_item("version", pkt.header.version)?;
285 out.set_item("command", pkt.header.command)?;
286 out.set_item("command_name", command_name(pkt.header.command))?;
287 out.set_item("payload_length", pkt.header.payload_length)?;
288
289 let flags = PyDict::new(py);
290 flags.set_item("raw", pkt.header.flags.raw)?;
291 flags.set_item("is_application", pkt.header.flags.is_application)?;
292 flags.set_item("is_control", pkt.header.flags.is_control)?;
293 flags.set_item("is_segmented", pkt.header.flags.is_segmented)?;
294 flags.set_item("is_client", pkt.header.flags.is_client)?;
295 flags.set_item("is_server", pkt.header.flags.is_server)?;
296 flags.set_item("is_msb", pkt.header.flags.is_msb)?;
297 out.set_item("flags", flags)?;
298
299 out.set_item("payload", PyBytes::new(py, &data[8.min(data.len())..]))?;
300
301 let details = PyDict::new(py);
302 if let Some(cmd) = pkt.decode_payload() {
303 fill_command_details(py, &cmd, &details)?;
304 }
305 out.set_item("details", details)?;
306 Ok(out)
307}
308
309pub(crate) fn fill_command_details(
310 py: Python<'_>,
311 cmd: &spvirit_codec::epics_decode::PvaPacketCommand,
312 details: &Bound<'_, PyDict>,
313) -> PyResult<()> {
314 use spvirit_codec::epics_decode::PvaPacketCommand as C;
315
316 match cmd {
317 C::Control(p) => {
318 details.set_item("kind", "control")?;
319 details.set_item("command", p.command)?;
320 details.set_item("data", p.data)?;
321 }
322 C::Search(_) => {
323 details.set_item("kind", "search")?;
324 }
325 C::SearchResponse(p) => {
326 details.set_item("kind", "search_response")?;
327 details.set_item("guid", PyBytes::new(py, &p.guid))?;
328 details.set_item("seq", p.seq)?;
329 details.set_item("port", p.port)?;
330 details.set_item("protocol", &p.protocol)?;
331 details.set_item("found", p.found)?;
332 details.set_item("cids", PyList::new(py, p.cids.iter().cloned())?)?;
333 }
334 C::Beacon(_) => {
335 details.set_item("kind", "beacon")?;
336 }
337 C::ConnectionValidation(_) => {
338 details.set_item("kind", "connection_validation")?;
339 }
340 C::ConnectionValidated(_) => {
341 details.set_item("kind", "connection_validated")?;
342 }
343 C::AuthNZ(_) => {
344 details.set_item("kind", "authnz")?;
345 }
346 C::AclChange(_) => {
347 details.set_item("kind", "acl_change")?;
348 }
349 C::Op(p) => {
350 details.set_item("kind", "op")?;
351 details.set_item("command", p.command)?;
352 details.set_item("sid_or_cid", p.sid_or_cid)?;
353 details.set_item("ioid", p.ioid)?;
354 details.set_item("subcmd", p.subcmd)?;
355 details.set_item("is_server", p.is_server)?;
356 if let Some(status) = &p.status {
357 let s = PyDict::new(py);
358 s.set_item("code", status.code)?;
359 s.set_item("message", status.message.as_deref())?;
360 s.set_item("is_error", status.is_error())?;
361 details.set_item("status", s)?;
362 }
363 if let Some(intro) = &p.introspection {
364 details.set_item("introspection", PyStructureDesc::from_inner(intro.clone()))?;
365 }
366 }
367 C::CreateChannel(p) => {
368 details.set_item("kind", "create_channel")?;
369 details.set_item("is_server", p.is_server)?;
370 details.set_item("cid", p.cid)?;
371 details.set_item("sid", p.sid)?;
372 if !p.channels.is_empty() {
373 let pairs: Vec<(u32, String)> = p.channels.clone();
374 details.set_item("channels", pairs)?;
375 }
376 if let Some(status) = &p.status {
377 let s = PyDict::new(py);
378 s.set_item("code", status.code)?;
379 s.set_item("message", status.message.as_deref())?;
380 s.set_item("is_error", status.is_error())?;
381 details.set_item("status", s)?;
382 }
383 }
384 C::DestroyChannel(p) => {
385 details.set_item("kind", "destroy_channel")?;
386 details.set_item("sid", p.sid)?;
387 details.set_item("cid", p.cid)?;
388 }
389 C::GetField(p) => {
390 details.set_item("kind", "get_field")?;
391 details.set_item("is_server", p.is_server)?;
392 details.set_item("sid", p.sid)?;
393 details.set_item("ioid", p.ioid)?;
394 details.set_item("field_name", p.field_name.as_deref())?;
395 }
396 C::Message(_) => {
397 details.set_item("kind", "message")?;
398 }
399 C::MultipleData(_) => {
400 details.set_item("kind", "multiple_data")?;
401 }
402 C::CancelRequest(_) => {
403 details.set_item("kind", "cancel_request")?;
404 }
405 C::DestroyRequest(_) => {
406 details.set_item("kind", "destroy_request")?;
407 }
408 C::OriginTag(_) => {
409 details.set_item("kind", "origin_tag")?;
410 }
411 C::Echo(data) => {
412 details.set_item("kind", "echo")?;
413 details.set_item("data", PyBytes::new(py, data))?;
414 }
415 C::Unknown(_) => {
416 details.set_item("kind", "unknown")?;
417 }
418 }
419 Ok(())
420}
421
422fn py_to_decoded(obj: &Bound<'_, PyAny>) -> PyResult<DecodedValue> {
426 use pyo3::types::{PyBool, PyFloat, PyInt, PyString};
427
428 if obj.is_none() {
429 return Ok(DecodedValue::Null);
430 }
431 if let Ok(b) = obj.downcast::<PyBool>() {
432 return Ok(DecodedValue::Boolean(b.is_true()));
433 }
434 if obj.is_instance_of::<PyInt>() {
435 let v: i64 = obj.extract()?;
436 return Ok(DecodedValue::Int64(v));
437 }
438 if obj.is_instance_of::<PyFloat>() {
439 let v: f64 = obj.extract()?;
440 return Ok(DecodedValue::Float64(v));
441 }
442 if obj.is_instance_of::<PyString>() {
443 let v: String = obj.extract()?;
444 return Ok(DecodedValue::String(v));
445 }
446 if let Ok(b) = obj.downcast::<pyo3::types::PyBytes>() {
447 return Ok(DecodedValue::Raw(b.as_bytes().to_vec()));
448 }
449 if let Ok(list) = obj.downcast::<PyList>() {
450 let mut out = Vec::with_capacity(list.len());
451 for item in list.iter() {
452 out.push(py_to_decoded(&item)?);
453 }
454 return Ok(DecodedValue::Array(out));
455 }
456 if let Ok(dict) = obj.downcast::<PyDict>() {
457 let mut out = Vec::new();
458 for (k, v) in dict.iter() {
459 let key: String = k.extract()?;
460 out.push((key, py_to_decoded(&v)?));
461 }
462 return Ok(DecodedValue::Structure(out));
463 }
464 Err(decode_msg_to_py_err(format!(
465 "cannot convert {} to DecodedValue",
466 obj.get_type().name()?
467 )))
468}
469
470pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
472 let py = parent.py();
473 let m = PyModule::new(py, "codec")?;
474 m.add_class::<PyFieldDesc>()?;
475 m.add_class::<PyStructureDesc>()?;
476 m.add_function(wrap_pyfunction!(decode_introspection, &m)?)?;
477 m.add_function(wrap_pyfunction!(decode_value, &m)?)?;
478 m.add_function(wrap_pyfunction!(encode_pv_request, &m)?)?;
479 m.add_function(wrap_pyfunction!(encode_put_payload, &m)?)?;
480 m.add_function(wrap_pyfunction!(format_value, &m)?)?;
481 m.add_function(wrap_pyfunction!(extract_nt_value, &m)?)?;
482 m.add_function(wrap_pyfunction!(decode_packet, &m)?)?;
483 parent.add_submodule(&m)?;
484 py.import("sys")?
486 .getattr("modules")?
487 .set_item("spvirit.codec", &m)?;
488 Ok(())
489}