Skip to main content

spvirit_codec/
spvd_encode.rs

1//! PVD (pvData) Encoding Helpers
2//!
3//! Minimal encoder for NTScalar introspection and value updates.
4
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::spvd_decode::{FieldDesc, FieldType, StructureDesc, TypeCode};
8
9use spvirit_types::{
10    NdDimension, NtAlarm, NtAttribute, NtDisplay, NtNdArray, NtPayload, NtScalar, NtScalarArray,
11    NtTable, NtTableColumn, NtTimeStamp, ScalarArrayValue, ScalarValue,
12};
13
14fn count_structure_fields(desc: &StructureDesc) -> usize {
15    let mut count = 0;
16    for field in &desc.fields {
17        count += 1;
18        if let FieldType::Structure(nested) = &field.field_type {
19            count += count_structure_fields(nested);
20        }
21    }
22    count
23}
24
25pub fn encode_size_pvd(size: usize, is_be: bool) -> Vec<u8> {
26    crate::encode_common::encode_size(size, is_be)
27}
28
29pub fn encode_string_pvd(value: &str, is_be: bool) -> Vec<u8> {
30    crate::encode_common::encode_string(value, is_be)
31}
32
33pub fn encode_structure_desc(desc: &StructureDesc, is_be: bool) -> Vec<u8> {
34    let mut out = Vec::new();
35    let struct_id = desc.struct_id.clone().unwrap_or_default();
36    out.extend_from_slice(&encode_string_pvd(&struct_id, is_be));
37    out.extend_from_slice(&encode_size_pvd(desc.fields.len(), is_be));
38    for field in &desc.fields {
39        out.extend_from_slice(&encode_field_desc(field, is_be));
40    }
41    out
42}
43
44fn encode_field_desc(field: &FieldDesc, is_be: bool) -> Vec<u8> {
45    let mut out = Vec::new();
46    out.extend_from_slice(&encode_string_pvd(&field.name, is_be));
47    out.extend_from_slice(&encode_type_desc(&field.field_type, is_be));
48    out
49}
50
51fn encode_type_desc(field_type: &FieldType, is_be: bool) -> Vec<u8> {
52    let mut out = Vec::new();
53    match field_type {
54        FieldType::Structure(desc) => {
55            out.push(0x80);
56            out.extend_from_slice(&encode_structure_desc(desc, is_be));
57        }
58        FieldType::StructureArray(desc) => {
59            out.push(0x88);
60            out.push(0x80); // inner structure element tag
61            out.extend_from_slice(&encode_structure_desc(desc, is_be));
62        }
63        FieldType::Union(fields) => {
64            out.push(0x81);
65            let desc = StructureDesc {
66                struct_id: None,
67                fields: fields.clone(),
68            };
69            out.extend_from_slice(&encode_structure_desc(&desc, is_be));
70        }
71        FieldType::UnionArray(fields) => {
72            out.push(0x89);
73            out.push(0x81); // inner union element tag
74            let desc = StructureDesc {
75                struct_id: None,
76                fields: fields.clone(),
77            };
78            out.extend_from_slice(&encode_structure_desc(&desc, is_be));
79        }
80        FieldType::Variant => out.push(0x82),
81        FieldType::VariantArray => out.push(0x8A),
82        FieldType::BoundedString(bound) => {
83            out.push(0x83);
84            out.extend_from_slice(&encode_size_pvd(*bound as usize, is_be));
85        }
86        FieldType::String => out.push(0x60),
87        FieldType::StringArray => out.push(0x68),
88        FieldType::Scalar(tc) => out.push(*tc as u8),
89        FieldType::ScalarArray(tc) => out.push((*tc as u8) | 0x08),
90    }
91    out
92}
93
94fn encode_scalar_value(value: &ScalarValue, is_be: bool) -> Vec<u8> {
95    match value {
96        ScalarValue::Bool(v) => vec![if *v { 1 } else { 0 }],
97        ScalarValue::I8(v) => vec![*v as u8],
98        ScalarValue::I16(v) => {
99            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
100        }
101        ScalarValue::I32(v) => {
102            if is_be {
103                v.to_be_bytes().to_vec()
104            } else {
105                v.to_le_bytes().to_vec()
106            }
107        }
108        ScalarValue::I64(v) => {
109            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
110        }
111        ScalarValue::U8(v) => vec![*v],
112        ScalarValue::U16(v) => {
113            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
114        }
115        ScalarValue::U32(v) => {
116            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
117        }
118        ScalarValue::U64(v) => {
119            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
120        }
121        ScalarValue::F32(v) => {
122            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
123        }
124        ScalarValue::F64(v) => {
125            if is_be {
126                v.to_be_bytes().to_vec()
127            } else {
128                v.to_le_bytes().to_vec()
129            }
130        }
131        ScalarValue::Str(v) => encode_string_pvd(v, is_be),
132    }
133}
134
135fn encode_alarm(nt: &NtScalar, is_be: bool) -> Vec<u8> {
136    let mut out = Vec::new();
137    out.extend_from_slice(&encode_i32(nt.alarm_severity, is_be));
138    out.extend_from_slice(&encode_i32(nt.alarm_status, is_be));
139    out.extend_from_slice(&encode_string_pvd(&nt.alarm_message, is_be));
140    out
141}
142
143fn encode_bool(value: bool) -> Vec<u8> {
144    vec![if value { 1 } else { 0 }]
145}
146
147fn encode_string_array(values: &[String], is_be: bool) -> Vec<u8> {
148    let mut out = Vec::new();
149    out.extend_from_slice(&encode_size_pvd(values.len(), is_be));
150    for v in values {
151        out.extend_from_slice(&encode_string_pvd(v, is_be));
152    }
153    out
154}
155
156fn encode_enum(index: i32, choices: &[String], is_be: bool) -> Vec<u8> {
157    let mut out = Vec::new();
158    out.extend_from_slice(&encode_i32(index, is_be));
159    out.extend_from_slice(&encode_string_array(choices, is_be));
160    out
161}
162
163fn encode_timestamp(_nt: &NtScalar, is_be: bool) -> Vec<u8> {
164    let mut out = Vec::new();
165    let now = SystemTime::now()
166        .duration_since(UNIX_EPOCH)
167        .unwrap_or_default();
168    let seconds_past_epoch = now.as_secs() as i64;
169    let nanos = now.subsec_nanos() as i32;
170
171    out.extend_from_slice(&encode_i64(seconds_past_epoch, is_be));
172    out.extend_from_slice(&encode_i32(nanos, is_be));
173    out.extend_from_slice(&encode_i32(0, is_be)); // userTag
174    out
175}
176
177fn encode_display(nt: &NtScalar, is_be: bool) -> Vec<u8> {
178    let mut out = Vec::new();
179    out.extend_from_slice(&encode_f64(nt.display_low, is_be));
180    out.extend_from_slice(&encode_f64(nt.display_high, is_be));
181    out.extend_from_slice(&encode_string_pvd(&nt.display_description, is_be));
182    out.extend_from_slice(&encode_string_pvd(&nt.units, is_be));
183    out.extend_from_slice(&encode_i32(nt.display_precision, is_be));
184    out.extend_from_slice(&encode_enum(
185        nt.display_form_index,
186        &nt.display_form_choices,
187        is_be,
188    ));
189    out
190}
191
192fn encode_control(nt: &NtScalar, is_be: bool) -> Vec<u8> {
193    let mut out = Vec::new();
194    out.extend_from_slice(&encode_f64(nt.control_low, is_be));
195    out.extend_from_slice(&encode_f64(nt.control_high, is_be));
196    out.extend_from_slice(&encode_f64(nt.control_min_step, is_be));
197    out
198}
199
200fn encode_value_alarm(nt: &NtScalar, is_be: bool) -> Vec<u8> {
201    let mut out = Vec::new();
202    out.extend_from_slice(&encode_bool(nt.value_alarm_active));
203    out.extend_from_slice(&encode_f64(nt.value_alarm_low_alarm_limit, is_be));
204    out.extend_from_slice(&encode_f64(nt.value_alarm_low_warning_limit, is_be));
205    out.extend_from_slice(&encode_f64(nt.value_alarm_high_warning_limit, is_be));
206    out.extend_from_slice(&encode_f64(nt.value_alarm_high_alarm_limit, is_be));
207    out.extend_from_slice(&encode_i32(nt.value_alarm_low_alarm_severity, is_be));
208    out.extend_from_slice(&encode_i32(nt.value_alarm_low_warning_severity, is_be));
209    out.extend_from_slice(&encode_i32(nt.value_alarm_high_warning_severity, is_be));
210    out.extend_from_slice(&encode_i32(nt.value_alarm_high_alarm_severity, is_be));
211    out.push(nt.value_alarm_hysteresis);
212    out
213}
214
215fn encode_i32(value: i32, is_be: bool) -> Vec<u8> {
216    if is_be {
217        value.to_be_bytes().to_vec()
218    } else {
219        value.to_le_bytes().to_vec()
220    }
221}
222
223fn encode_i64(value: i64, is_be: bool) -> Vec<u8> {
224    if is_be {
225        value.to_be_bytes().to_vec()
226    } else {
227        value.to_le_bytes().to_vec()
228    }
229}
230
231fn encode_f64(value: f64, is_be: bool) -> Vec<u8> {
232    if is_be {
233        value.to_be_bytes().to_vec()
234    } else {
235        value.to_le_bytes().to_vec()
236    }
237}
238
239pub fn nt_scalar_desc(value: &ScalarValue) -> StructureDesc {
240    let value_type = match value {
241        ScalarValue::Bool(_) => FieldType::Scalar(TypeCode::Boolean),
242        ScalarValue::I8(_) => FieldType::Scalar(TypeCode::Int8),
243        ScalarValue::I16(_) => FieldType::Scalar(TypeCode::Int16),
244        ScalarValue::I32(_) => FieldType::Scalar(TypeCode::Int32),
245        ScalarValue::I64(_) => FieldType::Scalar(TypeCode::Int64),
246        ScalarValue::U8(_) => FieldType::Scalar(TypeCode::UInt8),
247        ScalarValue::U16(_) => FieldType::Scalar(TypeCode::UInt16),
248        ScalarValue::U32(_) => FieldType::Scalar(TypeCode::UInt32),
249        ScalarValue::U64(_) => FieldType::Scalar(TypeCode::UInt64),
250        ScalarValue::F32(_) => FieldType::Scalar(TypeCode::Float32),
251        ScalarValue::F64(_) => FieldType::Scalar(TypeCode::Float64),
252        ScalarValue::Str(_) => FieldType::String,
253    };
254
255    StructureDesc {
256        struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
257        fields: vec![
258            FieldDesc {
259                name: "value".to_string(),
260                field_type: value_type,
261            },
262            FieldDesc {
263                name: "alarm".to_string(),
264                field_type: FieldType::Structure(StructureDesc {
265                    struct_id: Some("alarm_t".to_string()),
266                    fields: vec![
267                        FieldDesc {
268                            name: "severity".to_string(),
269                            field_type: FieldType::Scalar(TypeCode::Int32),
270                        },
271                        FieldDesc {
272                            name: "status".to_string(),
273                            field_type: FieldType::Scalar(TypeCode::Int32),
274                        },
275                        FieldDesc {
276                            name: "message".to_string(),
277                            field_type: FieldType::String,
278                        },
279                    ],
280                }),
281            },
282            FieldDesc {
283                name: "timeStamp".to_string(),
284                field_type: FieldType::Structure(StructureDesc {
285                    struct_id: None,
286                    fields: vec![
287                        FieldDesc {
288                            name: "secondsPastEpoch".to_string(),
289                            field_type: FieldType::Scalar(TypeCode::Int64),
290                        },
291                        FieldDesc {
292                            name: "nanoseconds".to_string(),
293                            field_type: FieldType::Scalar(TypeCode::Int32),
294                        },
295                        FieldDesc {
296                            name: "userTag".to_string(),
297                            field_type: FieldType::Scalar(TypeCode::Int32),
298                        },
299                    ],
300                }),
301            },
302            FieldDesc {
303                name: "display".to_string(),
304                field_type: FieldType::Structure(StructureDesc {
305                    struct_id: None,
306                    fields: vec![
307                        FieldDesc {
308                            name: "limitLow".to_string(),
309                            field_type: FieldType::Scalar(TypeCode::Float64),
310                        },
311                        FieldDesc {
312                            name: "limitHigh".to_string(),
313                            field_type: FieldType::Scalar(TypeCode::Float64),
314                        },
315                        FieldDesc {
316                            name: "description".to_string(),
317                            field_type: FieldType::String,
318                        },
319                        FieldDesc {
320                            name: "units".to_string(),
321                            field_type: FieldType::String,
322                        },
323                        FieldDesc {
324                            name: "precision".to_string(),
325                            field_type: FieldType::Scalar(TypeCode::Int32),
326                        },
327                        FieldDesc {
328                            name: "form".to_string(),
329                            field_type: FieldType::Structure(StructureDesc {
330                                struct_id: Some("enum_t".to_string()),
331                                fields: vec![
332                                    FieldDesc {
333                                        name: "index".to_string(),
334                                        field_type: FieldType::Scalar(TypeCode::Int32),
335                                    },
336                                    FieldDesc {
337                                        name: "choices".to_string(),
338                                        field_type: FieldType::StringArray,
339                                    },
340                                ],
341                            }),
342                        },
343                    ],
344                }),
345            },
346            FieldDesc {
347                name: "control".to_string(),
348                field_type: FieldType::Structure(StructureDesc {
349                    struct_id: Some("control_t".to_string()),
350                    fields: vec![
351                        FieldDesc {
352                            name: "limitLow".to_string(),
353                            field_type: FieldType::Scalar(TypeCode::Float64),
354                        },
355                        FieldDesc {
356                            name: "limitHigh".to_string(),
357                            field_type: FieldType::Scalar(TypeCode::Float64),
358                        },
359                        FieldDesc {
360                            name: "minStep".to_string(),
361                            field_type: FieldType::Scalar(TypeCode::Float64),
362                        },
363                    ],
364                }),
365            },
366            FieldDesc {
367                name: "valueAlarm".to_string(),
368                field_type: FieldType::Structure(StructureDesc {
369                    struct_id: Some("valueAlarm_t".to_string()),
370                    fields: vec![
371                        FieldDesc {
372                            name: "active".to_string(),
373                            field_type: FieldType::Scalar(TypeCode::Boolean),
374                        },
375                        FieldDesc {
376                            name: "lowAlarmLimit".to_string(),
377                            field_type: FieldType::Scalar(TypeCode::Float64),
378                        },
379                        FieldDesc {
380                            name: "lowWarningLimit".to_string(),
381                            field_type: FieldType::Scalar(TypeCode::Float64),
382                        },
383                        FieldDesc {
384                            name: "highWarningLimit".to_string(),
385                            field_type: FieldType::Scalar(TypeCode::Float64),
386                        },
387                        FieldDesc {
388                            name: "highAlarmLimit".to_string(),
389                            field_type: FieldType::Scalar(TypeCode::Float64),
390                        },
391                        FieldDesc {
392                            name: "lowAlarmSeverity".to_string(),
393                            field_type: FieldType::Scalar(TypeCode::Int32),
394                        },
395                        FieldDesc {
396                            name: "lowWarningSeverity".to_string(),
397                            field_type: FieldType::Scalar(TypeCode::Int32),
398                        },
399                        FieldDesc {
400                            name: "highWarningSeverity".to_string(),
401                            field_type: FieldType::Scalar(TypeCode::Int32),
402                        },
403                        FieldDesc {
404                            name: "highAlarmSeverity".to_string(),
405                            field_type: FieldType::Scalar(TypeCode::Int32),
406                        },
407                        FieldDesc {
408                            name: "hysteresis".to_string(),
409                            field_type: FieldType::Scalar(TypeCode::UInt8),
410                        },
411                    ],
412                }),
413            },
414        ],
415    }
416}
417
418pub fn encode_nt_scalar_full(nt: &NtScalar, is_be: bool) -> Vec<u8> {
419    let mut out = Vec::new();
420    out.extend_from_slice(&encode_scalar_value(&nt.value, is_be));
421    out.extend_from_slice(&encode_alarm(nt, is_be));
422    out.extend_from_slice(&encode_timestamp(nt, is_be));
423    out.extend_from_slice(&encode_display(nt, is_be));
424    out.extend_from_slice(&encode_control(nt, is_be));
425    out.extend_from_slice(&encode_value_alarm(nt, is_be));
426    out
427}
428
429fn encode_structure_bitset(desc: &StructureDesc, is_be: bool) -> Vec<u8> {
430    let total_bits = 1 + count_structure_fields(desc);
431    let bitset_size = (total_bits + 7) / 8;
432    let mut bitset = vec![0u8; bitset_size];
433    for bit in 0..total_bits {
434        let byte_idx = bit / 8;
435        let bit_idx = bit % 8;
436        bitset[byte_idx] |= 1 << bit_idx;
437    }
438    let mut out = Vec::new();
439    out.extend_from_slice(&encode_size_pvd(bitset_size, is_be));
440    out.extend_from_slice(&bitset);
441    out
442}
443
444fn encode_structure_with_bitset(desc: &StructureDesc, nt: &NtScalar, is_be: bool) -> Vec<u8> {
445    let mut out = Vec::new();
446    out.extend_from_slice(&encode_structure_bitset(desc, is_be));
447    out.extend_from_slice(&encode_nt_scalar_full(nt, is_be));
448    out
449}
450
451pub fn encode_nt_scalar_bitset(nt: &NtScalar, is_be: bool) -> Vec<u8> {
452    let desc = nt_scalar_desc(&nt.value);
453    encode_structure_with_bitset(&desc, nt, is_be)
454}
455
456pub fn encode_nt_scalar_bitset_parts(nt: &NtScalar, is_be: bool) -> (Vec<u8>, Vec<u8>) {
457    let desc = nt_scalar_desc(&nt.value);
458    let bitset = encode_structure_bitset(&desc, is_be);
459    let values = encode_nt_scalar_full(nt, is_be);
460    (bitset, values)
461}
462
463fn alarm_desc() -> StructureDesc {
464    StructureDesc {
465        struct_id: Some("alarm_t".to_string()),
466        fields: vec![
467            FieldDesc {
468                name: "severity".to_string(),
469                field_type: FieldType::Scalar(TypeCode::Int32),
470            },
471            FieldDesc {
472                name: "status".to_string(),
473                field_type: FieldType::Scalar(TypeCode::Int32),
474            },
475            FieldDesc {
476                name: "message".to_string(),
477                field_type: FieldType::String,
478            },
479        ],
480    }
481}
482
483fn timestamp_desc() -> StructureDesc {
484    StructureDesc {
485        struct_id: Some("time_t".to_string()),
486        fields: vec![
487            FieldDesc {
488                name: "secondsPastEpoch".to_string(),
489                field_type: FieldType::Scalar(TypeCode::Int64),
490            },
491            FieldDesc {
492                name: "nanoseconds".to_string(),
493                field_type: FieldType::Scalar(TypeCode::Int32),
494            },
495            FieldDesc {
496                name: "userTag".to_string(),
497                field_type: FieldType::Scalar(TypeCode::Int32),
498            },
499        ],
500    }
501}
502
503fn display_desc() -> StructureDesc {
504    StructureDesc {
505        struct_id: Some("display_t".to_string()),
506        fields: vec![
507            FieldDesc {
508                name: "limitLow".to_string(),
509                field_type: FieldType::Scalar(TypeCode::Float64),
510            },
511            FieldDesc {
512                name: "limitHigh".to_string(),
513                field_type: FieldType::Scalar(TypeCode::Float64),
514            },
515            FieldDesc {
516                name: "description".to_string(),
517                field_type: FieldType::String,
518            },
519            FieldDesc {
520                name: "units".to_string(),
521                field_type: FieldType::String,
522            },
523            FieldDesc {
524                name: "precision".to_string(),
525                field_type: FieldType::Scalar(TypeCode::Int32),
526            },
527        ],
528    }
529}
530
531fn scalar_array_field_type(value: &ScalarArrayValue) -> FieldType {
532    match value {
533        ScalarArrayValue::Bool(_) => FieldType::ScalarArray(TypeCode::Boolean),
534        ScalarArrayValue::I8(_) => FieldType::ScalarArray(TypeCode::Int8),
535        ScalarArrayValue::I16(_) => FieldType::ScalarArray(TypeCode::Int16),
536        ScalarArrayValue::I32(_) => FieldType::ScalarArray(TypeCode::Int32),
537        ScalarArrayValue::I64(_) => FieldType::ScalarArray(TypeCode::Int64),
538        ScalarArrayValue::U8(_) => FieldType::ScalarArray(TypeCode::UInt8),
539        ScalarArrayValue::U16(_) => FieldType::ScalarArray(TypeCode::UInt16),
540        ScalarArrayValue::U32(_) => FieldType::ScalarArray(TypeCode::UInt32),
541        ScalarArrayValue::U64(_) => FieldType::ScalarArray(TypeCode::UInt64),
542        ScalarArrayValue::F32(_) => FieldType::ScalarArray(TypeCode::Float32),
543        ScalarArrayValue::F64(_) => FieldType::ScalarArray(TypeCode::Float64),
544        ScalarArrayValue::Str(_) => FieldType::StringArray,
545    }
546}
547
548fn encode_scalar_array_value_pvd(value: &ScalarArrayValue, is_be: bool) -> Vec<u8> {
549    let mut out = Vec::new();
550    match value {
551        ScalarArrayValue::Bool(v) => {
552            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
553            for i in v {
554                out.push(if *i { 1 } else { 0 });
555            }
556        }
557        ScalarArrayValue::I8(v) => {
558            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
559            for i in v {
560                out.push(*i as u8);
561            }
562        }
563        ScalarArrayValue::I16(v) => {
564            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
565            for i in v {
566                let b = if is_be {
567                    i.to_be_bytes()
568                } else {
569                    i.to_le_bytes()
570                };
571                out.extend_from_slice(&b);
572            }
573        }
574        ScalarArrayValue::I32(v) => {
575            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
576            for i in v {
577                out.extend_from_slice(&encode_i32(*i, is_be));
578            }
579        }
580        ScalarArrayValue::I64(v) => {
581            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
582            for i in v {
583                out.extend_from_slice(&encode_i64(*i, is_be));
584            }
585        }
586        ScalarArrayValue::U8(v) => {
587            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
588            out.extend_from_slice(v);
589        }
590        ScalarArrayValue::U16(v) => {
591            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
592            for i in v {
593                let b = if is_be {
594                    i.to_be_bytes()
595                } else {
596                    i.to_le_bytes()
597                };
598                out.extend_from_slice(&b);
599            }
600        }
601        ScalarArrayValue::U32(v) => {
602            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
603            for i in v {
604                let b = if is_be {
605                    i.to_be_bytes()
606                } else {
607                    i.to_le_bytes()
608                };
609                out.extend_from_slice(&b);
610            }
611        }
612        ScalarArrayValue::U64(v) => {
613            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
614            for i in v {
615                let b = if is_be {
616                    i.to_be_bytes()
617                } else {
618                    i.to_le_bytes()
619                };
620                out.extend_from_slice(&b);
621            }
622        }
623        ScalarArrayValue::F32(v) => {
624            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
625            for i in v {
626                let b = if is_be {
627                    i.to_be_bytes()
628                } else {
629                    i.to_le_bytes()
630                };
631                out.extend_from_slice(&b);
632            }
633        }
634        ScalarArrayValue::F64(v) => {
635            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
636            for i in v {
637                out.extend_from_slice(&encode_f64(*i, is_be));
638            }
639        }
640        ScalarArrayValue::Str(v) => {
641            out.extend_from_slice(&encode_string_array(v, is_be));
642        }
643    }
644    out
645}
646
647fn encode_nt_alarm(alarm: &NtAlarm, is_be: bool) -> Vec<u8> {
648    let mut out = Vec::new();
649    out.extend_from_slice(&encode_i32(alarm.severity, is_be));
650    out.extend_from_slice(&encode_i32(alarm.status, is_be));
651    out.extend_from_slice(&encode_string_pvd(&alarm.message, is_be));
652    out
653}
654
655fn encode_nt_timestamp(ts: &NtTimeStamp, is_be: bool) -> Vec<u8> {
656    let mut out = Vec::new();
657    out.extend_from_slice(&encode_i64(ts.seconds_past_epoch, is_be));
658    out.extend_from_slice(&encode_i32(ts.nanoseconds, is_be));
659    out.extend_from_slice(&encode_i32(ts.user_tag, is_be));
660    out
661}
662
663fn encode_nt_display(display: &NtDisplay, is_be: bool) -> Vec<u8> {
664    let mut out = Vec::new();
665    out.extend_from_slice(&encode_f64(display.limit_low, is_be));
666    out.extend_from_slice(&encode_f64(display.limit_high, is_be));
667    out.extend_from_slice(&encode_string_pvd(&display.description, is_be));
668    out.extend_from_slice(&encode_string_pvd(&display.units, is_be));
669    out.extend_from_slice(&encode_i32(display.precision, is_be));
670    out
671}
672
673pub fn nt_scalar_array_desc(value: &ScalarArrayValue) -> StructureDesc {
674    StructureDesc {
675        struct_id: Some("epics:nt/NTScalarArray:1.0".to_string()),
676        fields: vec![
677            FieldDesc {
678                name: "value".to_string(),
679                field_type: scalar_array_field_type(value),
680            },
681            FieldDesc {
682                name: "alarm".to_string(),
683                field_type: FieldType::Structure(alarm_desc()),
684            },
685            FieldDesc {
686                name: "timeStamp".to_string(),
687                field_type: FieldType::Structure(timestamp_desc()),
688            },
689            FieldDesc {
690                name: "display".to_string(),
691                field_type: FieldType::Structure(display_desc()),
692            },
693            FieldDesc {
694                name: "control".to_string(),
695                field_type: FieldType::Structure(StructureDesc {
696                    struct_id: Some("control_t".to_string()),
697                    fields: vec![
698                        FieldDesc {
699                            name: "limitLow".to_string(),
700                            field_type: FieldType::Scalar(TypeCode::Float64),
701                        },
702                        FieldDesc {
703                            name: "limitHigh".to_string(),
704                            field_type: FieldType::Scalar(TypeCode::Float64),
705                        },
706                        FieldDesc {
707                            name: "minStep".to_string(),
708                            field_type: FieldType::Scalar(TypeCode::Float64),
709                        },
710                    ],
711                }),
712            },
713        ],
714    }
715}
716
717pub fn encode_nt_scalar_array_full(nt: &NtScalarArray, is_be: bool) -> Vec<u8> {
718    let mut out = Vec::new();
719    out.extend_from_slice(&encode_scalar_array_value_pvd(&nt.value, is_be));
720    out.extend_from_slice(&encode_nt_alarm(&nt.alarm, is_be));
721    out.extend_from_slice(&encode_nt_timestamp(&nt.time_stamp, is_be));
722    out.extend_from_slice(&encode_nt_display(&nt.display, is_be));
723    out.extend_from_slice(&encode_f64(nt.control.limit_low, is_be));
724    out.extend_from_slice(&encode_f64(nt.control.limit_high, is_be));
725    out.extend_from_slice(&encode_f64(nt.control.min_step, is_be));
726    out
727}
728
729pub fn nt_table_desc(nt: &NtTable) -> StructureDesc {
730    let mut value_fields: Vec<FieldDesc> = Vec::new();
731    for col in &nt.columns {
732        value_fields.push(FieldDesc {
733            name: col.name.clone(),
734            field_type: scalar_array_field_type(&col.values),
735        });
736    }
737    StructureDesc {
738        struct_id: Some("epics:nt/NTTable:1.0".to_string()),
739        fields: vec![
740            FieldDesc {
741                name: "labels".to_string(),
742                field_type: FieldType::StringArray,
743            },
744            FieldDesc {
745                name: "value".to_string(),
746                field_type: FieldType::Structure(StructureDesc {
747                    struct_id: None,
748                    fields: value_fields,
749                }),
750            },
751        ],
752    }
753}
754
755pub fn encode_nt_table_full(nt: &NtTable, is_be: bool) -> Vec<u8> {
756    let mut out = Vec::new();
757    out.extend_from_slice(&encode_string_array(&nt.labels, is_be));
758    for NtTableColumn { values, .. } in &nt.columns {
759        out.extend_from_slice(&encode_scalar_array_value_pvd(values, is_be));
760    }
761    out
762}
763
764fn nt_ndarray_value_union_fields() -> Vec<FieldDesc> {
765    vec![
766        FieldDesc {
767            name: "booleanValue".to_string(),
768            field_type: FieldType::ScalarArray(TypeCode::Boolean),
769        },
770        FieldDesc {
771            name: "byteValue".to_string(),
772            field_type: FieldType::ScalarArray(TypeCode::Int8),
773        },
774        FieldDesc {
775            name: "shortValue".to_string(),
776            field_type: FieldType::ScalarArray(TypeCode::Int16),
777        },
778        FieldDesc {
779            name: "intValue".to_string(),
780            field_type: FieldType::ScalarArray(TypeCode::Int32),
781        },
782        FieldDesc {
783            name: "longValue".to_string(),
784            field_type: FieldType::ScalarArray(TypeCode::Int64),
785        },
786        FieldDesc {
787            name: "ubyteValue".to_string(),
788            field_type: FieldType::ScalarArray(TypeCode::UInt8),
789        },
790        FieldDesc {
791            name: "ushortValue".to_string(),
792            field_type: FieldType::ScalarArray(TypeCode::UInt16),
793        },
794        FieldDesc {
795            name: "uintValue".to_string(),
796            field_type: FieldType::ScalarArray(TypeCode::UInt32),
797        },
798        FieldDesc {
799            name: "ulongValue".to_string(),
800            field_type: FieldType::ScalarArray(TypeCode::UInt64),
801        },
802        FieldDesc {
803            name: "floatValue".to_string(),
804            field_type: FieldType::ScalarArray(TypeCode::Float32),
805        },
806        FieldDesc {
807            name: "doubleValue".to_string(),
808            field_type: FieldType::ScalarArray(TypeCode::Float64),
809        },
810        FieldDesc {
811            name: "stringValue".to_string(),
812            field_type: FieldType::StringArray,
813        },
814    ]
815}
816
817fn ndarray_union_index(value: &ScalarArrayValue) -> usize {
818    match value {
819        ScalarArrayValue::Bool(_) => 0,
820        ScalarArrayValue::I8(_) => 1,
821        ScalarArrayValue::I16(_) => 2,
822        ScalarArrayValue::I32(_) => 3,
823        ScalarArrayValue::I64(_) => 4,
824        ScalarArrayValue::U8(_) => 5,
825        ScalarArrayValue::U16(_) => 6,
826        ScalarArrayValue::U32(_) => 7,
827        ScalarArrayValue::U64(_) => 8,
828        ScalarArrayValue::F32(_) => 9,
829        ScalarArrayValue::F64(_) => 10,
830        ScalarArrayValue::Str(_) => 11,
831    }
832}
833
834fn encode_ndarray_union(value: &ScalarArrayValue, is_be: bool) -> Vec<u8> {
835    let mut out = Vec::new();
836    out.extend_from_slice(&encode_size_pvd(ndarray_union_index(value), is_be));
837    out.extend_from_slice(&encode_scalar_array_value_pvd(value, is_be));
838    out
839}
840
841fn encode_codec_parameters(
842    parameters: &std::collections::HashMap<String, String>,
843    is_be: bool,
844) -> Vec<u8> {
845    if parameters.is_empty() {
846        return vec![0xFF];
847    }
848    let mut out = Vec::new();
849    out.push(0x80);
850    let mut fields = Vec::new();
851    for key in parameters.keys() {
852        fields.push(FieldDesc {
853            name: key.clone(),
854            field_type: FieldType::String,
855        });
856    }
857    let desc = StructureDesc {
858        struct_id: None,
859        fields,
860    };
861    out.extend_from_slice(&encode_structure_desc(&desc, is_be));
862    for value in parameters.values() {
863        out.extend_from_slice(&encode_string_pvd(value, is_be));
864    }
865    out
866}
867
868pub fn nt_ndarray_desc(_nt: &NtNdArray) -> StructureDesc {
869    StructureDesc {
870        struct_id: Some("epics:nt/NTNDArray:1.0".to_string()),
871        fields: vec![
872            FieldDesc {
873                name: "value".to_string(),
874                field_type: FieldType::Union(nt_ndarray_value_union_fields()),
875            },
876            FieldDesc {
877                name: "codec".to_string(),
878                field_type: FieldType::Structure(StructureDesc {
879                    struct_id: Some("codec_t".to_string()),
880                    fields: vec![
881                        FieldDesc {
882                            name: "name".to_string(),
883                            field_type: FieldType::String,
884                        },
885                        FieldDesc {
886                            name: "parameters".to_string(),
887                            field_type: FieldType::Variant,
888                        },
889                    ],
890                }),
891            },
892            FieldDesc {
893                name: "compressedSize".to_string(),
894                field_type: FieldType::Scalar(TypeCode::Int64),
895            },
896            FieldDesc {
897                name: "uncompressedSize".to_string(),
898                field_type: FieldType::Scalar(TypeCode::Int64),
899            },
900            FieldDesc {
901                name: "dimension".to_string(),
902                field_type: FieldType::StructureArray(StructureDesc {
903                    struct_id: Some("dimension_t".to_string()),
904                    fields: vec![
905                        FieldDesc {
906                            name: "size".to_string(),
907                            field_type: FieldType::Scalar(TypeCode::Int32),
908                        },
909                        FieldDesc {
910                            name: "offset".to_string(),
911                            field_type: FieldType::Scalar(TypeCode::Int32),
912                        },
913                        FieldDesc {
914                            name: "fullSize".to_string(),
915                            field_type: FieldType::Scalar(TypeCode::Int32),
916                        },
917                        FieldDesc {
918                            name: "binning".to_string(),
919                            field_type: FieldType::Scalar(TypeCode::Int32),
920                        },
921                        FieldDesc {
922                            name: "reverse".to_string(),
923                            field_type: FieldType::Scalar(TypeCode::Boolean),
924                        },
925                    ],
926                }),
927            },
928            FieldDesc {
929                name: "uniqueId".to_string(),
930                field_type: FieldType::Scalar(TypeCode::Int32),
931            },
932            FieldDesc {
933                name: "dataTimeStamp".to_string(),
934                field_type: FieldType::Structure(timestamp_desc()),
935            },
936            FieldDesc {
937                name: "attribute".to_string(),
938                field_type: FieldType::StructureArray(StructureDesc {
939                    struct_id: Some("NTAttribute".to_string()),
940                    fields: vec![
941                        FieldDesc {
942                            name: "name".to_string(),
943                            field_type: FieldType::String,
944                        },
945                        FieldDesc {
946                            name: "value".to_string(),
947                            field_type: FieldType::Variant,
948                        },
949                        FieldDesc {
950                            name: "descriptor".to_string(),
951                            field_type: FieldType::String,
952                        },
953                        FieldDesc {
954                            name: "sourceType".to_string(),
955                            field_type: FieldType::Scalar(TypeCode::Int32),
956                        },
957                        FieldDesc {
958                            name: "source".to_string(),
959                            field_type: FieldType::String,
960                        },
961                    ],
962                }),
963            },
964            FieldDesc {
965                name: "descriptor".to_string(),
966                field_type: FieldType::String,
967            },
968            FieldDesc {
969                name: "alarm".to_string(),
970                field_type: FieldType::Structure(alarm_desc()),
971            },
972            FieldDesc {
973                name: "timeStamp".to_string(),
974                field_type: FieldType::Structure(timestamp_desc()),
975            },
976            FieldDesc {
977                name: "display".to_string(),
978                field_type: FieldType::Structure(display_desc()),
979            },
980        ],
981    }
982}
983
984fn encode_attribute_variant(attr: &NtAttribute, is_be: bool) -> Vec<u8> {
985    match &attr.value {
986        ScalarValue::Bool(v) => {
987            let mut out = vec![TypeCode::Boolean as u8];
988            out.push(if *v { 1 } else { 0 });
989            out
990        }
991        ScalarValue::I8(v) => {
992            let mut out = vec![TypeCode::Int8 as u8];
993            out.push(*v as u8);
994            out
995        }
996        ScalarValue::I16(v) => {
997            let mut out = vec![TypeCode::Int16 as u8];
998            out.extend_from_slice(&if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() });
999            out
1000        }
1001        ScalarValue::I32(v) => {
1002            let mut out = vec![TypeCode::Int32 as u8];
1003            out.extend_from_slice(&encode_i32(*v, is_be));
1004            out
1005        }
1006        ScalarValue::I64(v) => {
1007            let mut out = vec![TypeCode::Int64 as u8];
1008            out.extend_from_slice(&encode_i64(*v, is_be));
1009            out
1010        }
1011        ScalarValue::U8(v) => {
1012            let mut out = vec![TypeCode::UInt8 as u8];
1013            out.push(*v);
1014            out
1015        }
1016        ScalarValue::U16(v) => {
1017            let mut out = vec![TypeCode::UInt16 as u8];
1018            out.extend_from_slice(&if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() });
1019            out
1020        }
1021        ScalarValue::U32(v) => {
1022            let mut out = vec![TypeCode::UInt32 as u8];
1023            out.extend_from_slice(&if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() });
1024            out
1025        }
1026        ScalarValue::U64(v) => {
1027            let mut out = vec![TypeCode::UInt64 as u8];
1028            out.extend_from_slice(&if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() });
1029            out
1030        }
1031        ScalarValue::F32(v) => {
1032            let mut out = vec![TypeCode::Float32 as u8];
1033            out.extend_from_slice(&if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() });
1034            out
1035        }
1036        ScalarValue::F64(v) => {
1037            let mut out = vec![TypeCode::Float64 as u8];
1038            out.extend_from_slice(&encode_f64(*v, is_be));
1039            out
1040        }
1041        ScalarValue::Str(v) => {
1042            let mut out = vec![TypeCode::String as u8];
1043            out.extend_from_slice(&encode_string_pvd(v, is_be));
1044            out
1045        }
1046    }
1047}
1048
1049pub fn encode_nt_ndarray_full(nt: &NtNdArray, is_be: bool) -> Vec<u8> {
1050    let mut out = Vec::new();
1051    out.extend_from_slice(&encode_ndarray_union(&nt.value, is_be));
1052    out.extend_from_slice(&encode_string_pvd(&nt.codec.name, is_be));
1053    out.extend_from_slice(&encode_codec_parameters(&nt.codec.parameters, is_be));
1054    out.extend_from_slice(&encode_i64(nt.compressed_size, is_be));
1055    out.extend_from_slice(&encode_i64(nt.uncompressed_size, is_be));
1056    out.extend_from_slice(&encode_size_pvd(nt.dimension.len(), is_be));
1057    for NdDimension {
1058        size,
1059        offset,
1060        full_size,
1061        binning,
1062        reverse,
1063    } in &nt.dimension
1064    {
1065        out.push(1); // non-null element indicator
1066        out.extend_from_slice(&encode_i32(*size, is_be));
1067        out.extend_from_slice(&encode_i32(*offset, is_be));
1068        out.extend_from_slice(&encode_i32(*full_size, is_be));
1069        out.extend_from_slice(&encode_i32(*binning, is_be));
1070        out.push(if *reverse { 1 } else { 0 });
1071    }
1072    out.extend_from_slice(&encode_i32(nt.unique_id, is_be));
1073    out.extend_from_slice(&encode_nt_timestamp(&nt.data_time_stamp, is_be));
1074    out.extend_from_slice(&encode_size_pvd(nt.attribute.len(), is_be));
1075    for attr in &nt.attribute {
1076        out.push(1); // non-null element indicator
1077        out.extend_from_slice(&encode_string_pvd(&attr.name, is_be));
1078        out.extend_from_slice(&encode_attribute_variant(attr, is_be));
1079        out.extend_from_slice(&encode_string_pvd(&attr.descriptor, is_be));
1080        out.extend_from_slice(&encode_i32(attr.source_type, is_be));
1081        out.extend_from_slice(&encode_string_pvd(&attr.source, is_be));
1082    }
1083    out.extend_from_slice(&encode_string_pvd(
1084        nt.descriptor.as_deref().unwrap_or(""),
1085        is_be,
1086    ));
1087    out.extend_from_slice(&encode_nt_alarm(
1088        nt.alarm.as_ref().unwrap_or(&NtAlarm::default()),
1089        is_be,
1090    ));
1091    out.extend_from_slice(&encode_nt_timestamp(
1092        nt.time_stamp.as_ref().unwrap_or(&NtTimeStamp::default()),
1093        is_be,
1094    ));
1095    out.extend_from_slice(&encode_nt_display(
1096        nt.display.as_ref().unwrap_or(&NtDisplay::default()),
1097        is_be,
1098    ));
1099    out
1100}
1101
1102pub fn nt_payload_desc(payload: &NtPayload) -> StructureDesc {
1103    match payload {
1104        NtPayload::Scalar(nt) => nt_scalar_desc(&nt.value),
1105        NtPayload::ScalarArray(nt) => nt_scalar_array_desc(&nt.value),
1106        NtPayload::Table(nt) => nt_table_desc(nt),
1107        NtPayload::NdArray(nt) => nt_ndarray_desc(nt),
1108    }
1109}
1110
1111pub fn encode_nt_payload_full(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1112    match payload {
1113        NtPayload::Scalar(nt) => encode_nt_scalar_full(nt, is_be),
1114        NtPayload::ScalarArray(nt) => encode_nt_scalar_array_full(nt, is_be),
1115        NtPayload::Table(nt) => encode_nt_table_full(nt, is_be),
1116        NtPayload::NdArray(nt) => encode_nt_ndarray_full(nt, is_be),
1117    }
1118}
1119
1120pub fn encode_nt_payload_bitset(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1121    let desc = nt_payload_desc(payload);
1122    let mut out = Vec::new();
1123    out.extend_from_slice(&encode_structure_bitset(&desc, is_be));
1124    out.extend_from_slice(&encode_nt_payload_full(payload, is_be));
1125    out
1126}
1127
1128pub fn encode_nt_payload_bitset_parts(payload: &NtPayload, is_be: bool) -> (Vec<u8>, Vec<u8>) {
1129    let desc = nt_payload_desc(payload);
1130    (
1131        encode_structure_bitset(&desc, is_be),
1132        encode_nt_payload_full(payload, is_be),
1133    )
1134}
1135
1136// ---------------------------------------------------------------------------
1137// Generic DecodedValue → wire bytes encoder
1138// ---------------------------------------------------------------------------
1139
1140use crate::spvd_decode::DecodedValue;
1141
1142/// Encode a `DecodedValue` back to PVA wire bytes.
1143pub fn encode_decoded_value(val: &DecodedValue, is_be: bool) -> Vec<u8> {
1144    match val {
1145        DecodedValue::Null => Vec::new(),
1146        DecodedValue::Boolean(v) => vec![if *v { 1 } else { 0 }],
1147        DecodedValue::Int8(v) => vec![*v as u8],
1148        DecodedValue::Int16(v) => {
1149            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
1150        }
1151        DecodedValue::Int32(v) => encode_i32(*v, is_be),
1152        DecodedValue::Int64(v) => encode_i64(*v, is_be),
1153        DecodedValue::UInt8(v) => vec![*v],
1154        DecodedValue::UInt16(v) => {
1155            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
1156        }
1157        DecodedValue::UInt32(v) => {
1158            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
1159        }
1160        DecodedValue::UInt64(v) => {
1161            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
1162        }
1163        DecodedValue::Float32(v) => {
1164            if is_be { v.to_be_bytes().to_vec() } else { v.to_le_bytes().to_vec() }
1165        }
1166        DecodedValue::Float64(v) => encode_f64(*v, is_be),
1167        DecodedValue::String(v) => encode_string_pvd(v, is_be),
1168        DecodedValue::Array(arr) => {
1169            let mut out = encode_size_pvd(arr.len(), is_be);
1170            for item in arr {
1171                out.extend_from_slice(&encode_decoded_value(item, is_be));
1172            }
1173            out
1174        }
1175        DecodedValue::Structure(fields) => {
1176            let mut out = Vec::new();
1177            for (_name, value) in fields {
1178                out.extend_from_slice(&encode_decoded_value(value, is_be));
1179            }
1180            out
1181        }
1182        DecodedValue::Raw(data) => data.clone(),
1183    }
1184}
1185
1186// ---------------------------------------------------------------------------
1187// pvRequest builder
1188// ---------------------------------------------------------------------------
1189
1190/// Build a pvRequest structure for the given top-level field names.
1191///
1192/// Produces the byte sequence that a client sends inside an INIT request to
1193/// select which fields to subscribe to, e.g.
1194/// `encode_pv_request(&["value", "alarm", "timeStamp"], false)` produces the
1195/// equivalent of `field(value,alarm,timeStamp)`.
1196///
1197/// The output is the *full* type-described pvRequest structure: a `0xFD` /
1198/// `0x80` tag followed by the structure descriptor and empty-struct field values.
1199pub fn encode_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
1200    // Build inner "field" structure descriptor: each requested field is an
1201    // empty sub-structure (no fields).
1202    let inner_fields: Vec<FieldDesc> = fields
1203        .iter()
1204        .map(|name| FieldDesc {
1205            name: name.to_string(),
1206            field_type: FieldType::Structure(StructureDesc {
1207                struct_id: None,
1208                fields: Vec::new(),
1209            }),
1210        })
1211        .collect();
1212
1213    let field_desc = StructureDesc {
1214        struct_id: None,
1215        fields: inner_fields,
1216    };
1217
1218    let pv_request_desc = StructureDesc {
1219        struct_id: None,
1220        fields: vec![FieldDesc {
1221            name: "field".to_string(),
1222            field_type: FieldType::Structure(field_desc),
1223        }],
1224    };
1225
1226    let mut out = Vec::new();
1227    out.push(0x80); // structure tag
1228    out.extend_from_slice(&encode_structure_desc(&pv_request_desc, is_be));
1229    // Values: the field structure and all its children are empty structs, so
1230    // there are no value bytes to write.
1231    out
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236    use super::*;
1237    use crate::spvd_decode::PvdDecoder;
1238
1239    #[test]
1240    fn nt_scalar_roundtrip() {
1241        let nt = NtScalar::from_value(ScalarValue::F64(12.5));
1242        let desc = nt_scalar_desc(&nt.value);
1243        let desc_bytes = encode_structure_desc(&desc, false);
1244        let mut pvd = Vec::new();
1245        pvd.push(0x80);
1246        pvd.extend_from_slice(&desc_bytes);
1247        pvd.extend_from_slice(&encode_nt_scalar_full(&nt, false));
1248
1249        let decoder = PvdDecoder::new(false);
1250        let parsed_desc = decoder.parse_introspection(&pvd).expect("desc");
1251        let (_, consumed) = decoder
1252            .decode_structure(&pvd[1 + desc_bytes.len()..], &parsed_desc)
1253            .expect("value");
1254        assert!(consumed > 0);
1255    }
1256
1257    #[test]
1258    fn nt_ndarray_roundtrip() {
1259        use spvirit_types::{NdCodec, NdDimension, NtAlarm, NtNdArray, NtTimeStamp, ScalarArrayValue};
1260        use std::collections::HashMap;
1261
1262        let nt = NtNdArray {
1263            value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
1264            codec: NdCodec {
1265                name: String::new(),
1266                parameters: HashMap::new(),
1267            },
1268            compressed_size: 4,
1269            uncompressed_size: 4,
1270            dimension: vec![NdDimension {
1271                size: 2,
1272                offset: 0,
1273                full_size: 2,
1274                binning: 1,
1275                reverse: false,
1276            }],
1277            unique_id: 42,
1278            data_time_stamp: NtTimeStamp {
1279                seconds_past_epoch: 1000,
1280                nanoseconds: 500,
1281                user_tag: 0,
1282            },
1283            attribute: Vec::new(),
1284            descriptor: Some("test".to_string()),
1285            alarm: Some(NtAlarm::default()),
1286            time_stamp: Some(NtTimeStamp::default()),
1287            display: None,
1288        };
1289
1290        let desc = nt_ndarray_desc(&nt);
1291        let desc_bytes = encode_structure_desc(&desc, false);
1292        let data_bytes = encode_nt_ndarray_full(&nt, false);
1293
1294        // Build complete PVD: type_tag + desc + data
1295        let mut pvd = Vec::new();
1296        pvd.push(0x80);
1297        pvd.extend_from_slice(&desc_bytes);
1298        pvd.extend_from_slice(&data_bytes);
1299
1300        let decoder = PvdDecoder::new(false);
1301        let parsed_desc = decoder.parse_introspection(&pvd).expect("desc parse failed");
1302        let data_start = 1 + desc_bytes.len();
1303        let (_decoded, consumed) = decoder
1304            .decode_structure(&pvd[data_start..], &parsed_desc)
1305            .expect("data decode failed");
1306        assert!(consumed > 0, "consumed should be > 0");
1307        assert_eq!(consumed, data_bytes.len(), "consumed should match data_bytes.len()");
1308    }
1309}