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, NtEnum, NtNdArray, NtPayload, NtScalar,
11    NtScalarArray, NtTable, NtTableColumn, NtTimeStamp, PvValue, 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 {
100                v.to_be_bytes().to_vec()
101            } else {
102                v.to_le_bytes().to_vec()
103            }
104        }
105        ScalarValue::I32(v) => {
106            if is_be {
107                v.to_be_bytes().to_vec()
108            } else {
109                v.to_le_bytes().to_vec()
110            }
111        }
112        ScalarValue::I64(v) => {
113            if is_be {
114                v.to_be_bytes().to_vec()
115            } else {
116                v.to_le_bytes().to_vec()
117            }
118        }
119        ScalarValue::U8(v) => vec![*v],
120        ScalarValue::U16(v) => {
121            if is_be {
122                v.to_be_bytes().to_vec()
123            } else {
124                v.to_le_bytes().to_vec()
125            }
126        }
127        ScalarValue::U32(v) => {
128            if is_be {
129                v.to_be_bytes().to_vec()
130            } else {
131                v.to_le_bytes().to_vec()
132            }
133        }
134        ScalarValue::U64(v) => {
135            if is_be {
136                v.to_be_bytes().to_vec()
137            } else {
138                v.to_le_bytes().to_vec()
139            }
140        }
141        ScalarValue::F32(v) => {
142            if is_be {
143                v.to_be_bytes().to_vec()
144            } else {
145                v.to_le_bytes().to_vec()
146            }
147        }
148        ScalarValue::F64(v) => {
149            if is_be {
150                v.to_be_bytes().to_vec()
151            } else {
152                v.to_le_bytes().to_vec()
153            }
154        }
155        ScalarValue::Str(v) => encode_string_pvd(v, is_be),
156    }
157}
158
159fn encode_alarm(nt: &NtScalar, is_be: bool) -> Vec<u8> {
160    let mut out = Vec::new();
161    out.extend_from_slice(&encode_i32(nt.alarm_severity, is_be));
162    out.extend_from_slice(&encode_i32(nt.alarm_status, is_be));
163    out.extend_from_slice(&encode_string_pvd(&nt.alarm_message, is_be));
164    out
165}
166
167fn encode_bool(value: bool) -> Vec<u8> {
168    vec![if value { 1 } else { 0 }]
169}
170
171fn encode_string_array(values: &[String], is_be: bool) -> Vec<u8> {
172    let mut out = Vec::new();
173    out.extend_from_slice(&encode_size_pvd(values.len(), is_be));
174    for v in values {
175        out.extend_from_slice(&encode_string_pvd(v, is_be));
176    }
177    out
178}
179
180fn encode_enum(index: i32, choices: &[String], is_be: bool) -> Vec<u8> {
181    let mut out = Vec::new();
182    out.extend_from_slice(&encode_i32(index, is_be));
183    out.extend_from_slice(&encode_string_array(choices, is_be));
184    out
185}
186
187fn encode_timestamp(_nt: &NtScalar, is_be: bool) -> Vec<u8> {
188    let mut out = Vec::new();
189    let now = SystemTime::now()
190        .duration_since(UNIX_EPOCH)
191        .unwrap_or_default();
192    let seconds_past_epoch = now.as_secs() as i64;
193    let nanos = now.subsec_nanos() as i32;
194
195    out.extend_from_slice(&encode_i64(seconds_past_epoch, is_be));
196    out.extend_from_slice(&encode_i32(nanos, is_be));
197    out.extend_from_slice(&encode_i32(0, is_be)); // userTag
198    out
199}
200
201fn encode_display(nt: &NtScalar, is_be: bool) -> Vec<u8> {
202    let mut out = Vec::new();
203    out.extend_from_slice(&encode_f64(nt.display_low, is_be));
204    out.extend_from_slice(&encode_f64(nt.display_high, is_be));
205    out.extend_from_slice(&encode_string_pvd(&nt.display_description, is_be));
206    out.extend_from_slice(&encode_string_pvd(&nt.units, is_be));
207    out.extend_from_slice(&encode_i32(nt.display_precision, is_be));
208    out.extend_from_slice(&encode_enum(
209        nt.display_form_index,
210        &nt.display_form_choices,
211        is_be,
212    ));
213    out
214}
215
216fn encode_control(nt: &NtScalar, is_be: bool) -> Vec<u8> {
217    let mut out = Vec::new();
218    out.extend_from_slice(&encode_f64(nt.control_low, is_be));
219    out.extend_from_slice(&encode_f64(nt.control_high, is_be));
220    out.extend_from_slice(&encode_f64(nt.control_min_step, is_be));
221    out
222}
223
224fn encode_value_alarm(nt: &NtScalar, is_be: bool) -> Vec<u8> {
225    let mut out = Vec::new();
226    out.extend_from_slice(&encode_bool(nt.value_alarm_active));
227    out.extend_from_slice(&encode_f64(nt.value_alarm_low_alarm_limit, is_be));
228    out.extend_from_slice(&encode_f64(nt.value_alarm_low_warning_limit, is_be));
229    out.extend_from_slice(&encode_f64(nt.value_alarm_high_warning_limit, is_be));
230    out.extend_from_slice(&encode_f64(nt.value_alarm_high_alarm_limit, is_be));
231    out.extend_from_slice(&encode_i32(nt.value_alarm_low_alarm_severity, is_be));
232    out.extend_from_slice(&encode_i32(nt.value_alarm_low_warning_severity, is_be));
233    out.extend_from_slice(&encode_i32(nt.value_alarm_high_warning_severity, is_be));
234    out.extend_from_slice(&encode_i32(nt.value_alarm_high_alarm_severity, is_be));
235    out.push(nt.value_alarm_hysteresis);
236    out
237}
238
239fn encode_i32(value: i32, is_be: bool) -> Vec<u8> {
240    if is_be {
241        value.to_be_bytes().to_vec()
242    } else {
243        value.to_le_bytes().to_vec()
244    }
245}
246
247fn encode_i64(value: i64, is_be: bool) -> Vec<u8> {
248    if is_be {
249        value.to_be_bytes().to_vec()
250    } else {
251        value.to_le_bytes().to_vec()
252    }
253}
254
255fn encode_f64(value: f64, is_be: bool) -> Vec<u8> {
256    if is_be {
257        value.to_be_bytes().to_vec()
258    } else {
259        value.to_le_bytes().to_vec()
260    }
261}
262
263pub fn nt_scalar_desc(value: &ScalarValue) -> StructureDesc {
264    let value_type = match value {
265        ScalarValue::Bool(_) => FieldType::Scalar(TypeCode::Boolean),
266        ScalarValue::I8(_) => FieldType::Scalar(TypeCode::Int8),
267        ScalarValue::I16(_) => FieldType::Scalar(TypeCode::Int16),
268        ScalarValue::I32(_) => FieldType::Scalar(TypeCode::Int32),
269        ScalarValue::I64(_) => FieldType::Scalar(TypeCode::Int64),
270        ScalarValue::U8(_) => FieldType::Scalar(TypeCode::UInt8),
271        ScalarValue::U16(_) => FieldType::Scalar(TypeCode::UInt16),
272        ScalarValue::U32(_) => FieldType::Scalar(TypeCode::UInt32),
273        ScalarValue::U64(_) => FieldType::Scalar(TypeCode::UInt64),
274        ScalarValue::F32(_) => FieldType::Scalar(TypeCode::Float32),
275        ScalarValue::F64(_) => FieldType::Scalar(TypeCode::Float64),
276        ScalarValue::Str(_) => FieldType::String,
277    };
278
279    StructureDesc {
280        struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
281        fields: vec![
282            FieldDesc {
283                name: "value".to_string(),
284                field_type: value_type,
285            },
286            FieldDesc {
287                name: "alarm".to_string(),
288                field_type: FieldType::Structure(StructureDesc {
289                    struct_id: Some("alarm_t".to_string()),
290                    fields: vec![
291                        FieldDesc {
292                            name: "severity".to_string(),
293                            field_type: FieldType::Scalar(TypeCode::Int32),
294                        },
295                        FieldDesc {
296                            name: "status".to_string(),
297                            field_type: FieldType::Scalar(TypeCode::Int32),
298                        },
299                        FieldDesc {
300                            name: "message".to_string(),
301                            field_type: FieldType::String,
302                        },
303                    ],
304                }),
305            },
306            FieldDesc {
307                name: "timeStamp".to_string(),
308                field_type: FieldType::Structure(StructureDesc {
309                    struct_id: None,
310                    fields: vec![
311                        FieldDesc {
312                            name: "secondsPastEpoch".to_string(),
313                            field_type: FieldType::Scalar(TypeCode::Int64),
314                        },
315                        FieldDesc {
316                            name: "nanoseconds".to_string(),
317                            field_type: FieldType::Scalar(TypeCode::Int32),
318                        },
319                        FieldDesc {
320                            name: "userTag".to_string(),
321                            field_type: FieldType::Scalar(TypeCode::Int32),
322                        },
323                    ],
324                }),
325            },
326            FieldDesc {
327                name: "display".to_string(),
328                field_type: FieldType::Structure(StructureDesc {
329                    struct_id: None,
330                    fields: vec![
331                        FieldDesc {
332                            name: "limitLow".to_string(),
333                            field_type: FieldType::Scalar(TypeCode::Float64),
334                        },
335                        FieldDesc {
336                            name: "limitHigh".to_string(),
337                            field_type: FieldType::Scalar(TypeCode::Float64),
338                        },
339                        FieldDesc {
340                            name: "description".to_string(),
341                            field_type: FieldType::String,
342                        },
343                        FieldDesc {
344                            name: "units".to_string(),
345                            field_type: FieldType::String,
346                        },
347                        FieldDesc {
348                            name: "precision".to_string(),
349                            field_type: FieldType::Scalar(TypeCode::Int32),
350                        },
351                        FieldDesc {
352                            name: "form".to_string(),
353                            field_type: FieldType::Structure(StructureDesc {
354                                struct_id: Some("enum_t".to_string()),
355                                fields: vec![
356                                    FieldDesc {
357                                        name: "index".to_string(),
358                                        field_type: FieldType::Scalar(TypeCode::Int32),
359                                    },
360                                    FieldDesc {
361                                        name: "choices".to_string(),
362                                        field_type: FieldType::StringArray,
363                                    },
364                                ],
365                            }),
366                        },
367                    ],
368                }),
369            },
370            FieldDesc {
371                name: "control".to_string(),
372                field_type: FieldType::Structure(StructureDesc {
373                    struct_id: Some("control_t".to_string()),
374                    fields: vec![
375                        FieldDesc {
376                            name: "limitLow".to_string(),
377                            field_type: FieldType::Scalar(TypeCode::Float64),
378                        },
379                        FieldDesc {
380                            name: "limitHigh".to_string(),
381                            field_type: FieldType::Scalar(TypeCode::Float64),
382                        },
383                        FieldDesc {
384                            name: "minStep".to_string(),
385                            field_type: FieldType::Scalar(TypeCode::Float64),
386                        },
387                    ],
388                }),
389            },
390            FieldDesc {
391                name: "valueAlarm".to_string(),
392                field_type: FieldType::Structure(StructureDesc {
393                    struct_id: Some("valueAlarm_t".to_string()),
394                    fields: vec![
395                        FieldDesc {
396                            name: "active".to_string(),
397                            field_type: FieldType::Scalar(TypeCode::Boolean),
398                        },
399                        FieldDesc {
400                            name: "lowAlarmLimit".to_string(),
401                            field_type: FieldType::Scalar(TypeCode::Float64),
402                        },
403                        FieldDesc {
404                            name: "lowWarningLimit".to_string(),
405                            field_type: FieldType::Scalar(TypeCode::Float64),
406                        },
407                        FieldDesc {
408                            name: "highWarningLimit".to_string(),
409                            field_type: FieldType::Scalar(TypeCode::Float64),
410                        },
411                        FieldDesc {
412                            name: "highAlarmLimit".to_string(),
413                            field_type: FieldType::Scalar(TypeCode::Float64),
414                        },
415                        FieldDesc {
416                            name: "lowAlarmSeverity".to_string(),
417                            field_type: FieldType::Scalar(TypeCode::Int32),
418                        },
419                        FieldDesc {
420                            name: "lowWarningSeverity".to_string(),
421                            field_type: FieldType::Scalar(TypeCode::Int32),
422                        },
423                        FieldDesc {
424                            name: "highWarningSeverity".to_string(),
425                            field_type: FieldType::Scalar(TypeCode::Int32),
426                        },
427                        FieldDesc {
428                            name: "highAlarmSeverity".to_string(),
429                            field_type: FieldType::Scalar(TypeCode::Int32),
430                        },
431                        FieldDesc {
432                            name: "hysteresis".to_string(),
433                            field_type: FieldType::Scalar(TypeCode::UInt8),
434                        },
435                    ],
436                }),
437            },
438        ],
439    }
440}
441
442pub fn encode_nt_scalar_full(nt: &NtScalar, is_be: bool) -> Vec<u8> {
443    let mut out = Vec::new();
444    out.extend_from_slice(&encode_scalar_value(&nt.value, is_be));
445    out.extend_from_slice(&encode_alarm(nt, is_be));
446    out.extend_from_slice(&encode_timestamp(nt, is_be));
447    out.extend_from_slice(&encode_display(nt, is_be));
448    out.extend_from_slice(&encode_control(nt, is_be));
449    out.extend_from_slice(&encode_value_alarm(nt, is_be));
450    out
451}
452
453fn encode_structure_bitset(desc: &StructureDesc, is_be: bool) -> Vec<u8> {
454    let total_bits = 1 + count_structure_fields(desc);
455    let bitset_size = (total_bits + 7) / 8;
456    let mut bitset = vec![0u8; bitset_size];
457    for bit in 0..total_bits {
458        let byte_idx = bit / 8;
459        let bit_idx = bit % 8;
460        bitset[byte_idx] |= 1 << bit_idx;
461    }
462    let mut out = Vec::new();
463    out.extend_from_slice(&encode_size_pvd(bitset_size, is_be));
464    out.extend_from_slice(&bitset);
465    out
466}
467
468fn encode_structure_with_bitset(desc: &StructureDesc, nt: &NtScalar, is_be: bool) -> Vec<u8> {
469    let mut out = Vec::new();
470    out.extend_from_slice(&encode_structure_bitset(desc, is_be));
471    out.extend_from_slice(&encode_nt_scalar_full(nt, is_be));
472    out
473}
474
475pub fn encode_nt_scalar_bitset(nt: &NtScalar, is_be: bool) -> Vec<u8> {
476    let desc = nt_scalar_desc(&nt.value);
477    encode_structure_with_bitset(&desc, nt, is_be)
478}
479
480pub fn encode_nt_scalar_bitset_parts(nt: &NtScalar, is_be: bool) -> (Vec<u8>, Vec<u8>) {
481    let desc = nt_scalar_desc(&nt.value);
482    let bitset = encode_structure_bitset(&desc, is_be);
483    let values = encode_nt_scalar_full(nt, is_be);
484    (bitset, values)
485}
486
487fn alarm_desc() -> StructureDesc {
488    StructureDesc {
489        struct_id: Some("alarm_t".to_string()),
490        fields: vec![
491            FieldDesc {
492                name: "severity".to_string(),
493                field_type: FieldType::Scalar(TypeCode::Int32),
494            },
495            FieldDesc {
496                name: "status".to_string(),
497                field_type: FieldType::Scalar(TypeCode::Int32),
498            },
499            FieldDesc {
500                name: "message".to_string(),
501                field_type: FieldType::String,
502            },
503        ],
504    }
505}
506
507fn timestamp_desc() -> StructureDesc {
508    StructureDesc {
509        struct_id: Some("time_t".to_string()),
510        fields: vec![
511            FieldDesc {
512                name: "secondsPastEpoch".to_string(),
513                field_type: FieldType::Scalar(TypeCode::Int64),
514            },
515            FieldDesc {
516                name: "nanoseconds".to_string(),
517                field_type: FieldType::Scalar(TypeCode::Int32),
518            },
519            FieldDesc {
520                name: "userTag".to_string(),
521                field_type: FieldType::Scalar(TypeCode::Int32),
522            },
523        ],
524    }
525}
526
527fn display_desc() -> StructureDesc {
528    StructureDesc {
529        struct_id: Some("display_t".to_string()),
530        fields: vec![
531            FieldDesc {
532                name: "limitLow".to_string(),
533                field_type: FieldType::Scalar(TypeCode::Float64),
534            },
535            FieldDesc {
536                name: "limitHigh".to_string(),
537                field_type: FieldType::Scalar(TypeCode::Float64),
538            },
539            FieldDesc {
540                name: "description".to_string(),
541                field_type: FieldType::String,
542            },
543            FieldDesc {
544                name: "units".to_string(),
545                field_type: FieldType::String,
546            },
547            FieldDesc {
548                name: "precision".to_string(),
549                field_type: FieldType::Scalar(TypeCode::Int32),
550            },
551        ],
552    }
553}
554
555fn scalar_array_field_type(value: &ScalarArrayValue) -> FieldType {
556    match value {
557        ScalarArrayValue::Bool(_) => FieldType::ScalarArray(TypeCode::Boolean),
558        ScalarArrayValue::I8(_) => FieldType::ScalarArray(TypeCode::Int8),
559        ScalarArrayValue::I16(_) => FieldType::ScalarArray(TypeCode::Int16),
560        ScalarArrayValue::I32(_) => FieldType::ScalarArray(TypeCode::Int32),
561        ScalarArrayValue::I64(_) => FieldType::ScalarArray(TypeCode::Int64),
562        ScalarArrayValue::U8(_) => FieldType::ScalarArray(TypeCode::UInt8),
563        ScalarArrayValue::U16(_) => FieldType::ScalarArray(TypeCode::UInt16),
564        ScalarArrayValue::U32(_) => FieldType::ScalarArray(TypeCode::UInt32),
565        ScalarArrayValue::U64(_) => FieldType::ScalarArray(TypeCode::UInt64),
566        ScalarArrayValue::F32(_) => FieldType::ScalarArray(TypeCode::Float32),
567        ScalarArrayValue::F64(_) => FieldType::ScalarArray(TypeCode::Float64),
568        ScalarArrayValue::Str(_) => FieldType::StringArray,
569    }
570}
571
572fn encode_scalar_array_value_pvd(value: &ScalarArrayValue, is_be: bool) -> Vec<u8> {
573    let mut out = Vec::new();
574    match value {
575        ScalarArrayValue::Bool(v) => {
576            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
577            for i in v {
578                out.push(if *i { 1 } else { 0 });
579            }
580        }
581        ScalarArrayValue::I8(v) => {
582            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
583            for i in v {
584                out.push(*i as u8);
585            }
586        }
587        ScalarArrayValue::I16(v) => {
588            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
589            for i in v {
590                let b = if is_be {
591                    i.to_be_bytes()
592                } else {
593                    i.to_le_bytes()
594                };
595                out.extend_from_slice(&b);
596            }
597        }
598        ScalarArrayValue::I32(v) => {
599            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
600            for i in v {
601                out.extend_from_slice(&encode_i32(*i, is_be));
602            }
603        }
604        ScalarArrayValue::I64(v) => {
605            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
606            for i in v {
607                out.extend_from_slice(&encode_i64(*i, is_be));
608            }
609        }
610        ScalarArrayValue::U8(v) => {
611            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
612            out.extend_from_slice(v);
613        }
614        ScalarArrayValue::U16(v) => {
615            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
616            for i in v {
617                let b = if is_be {
618                    i.to_be_bytes()
619                } else {
620                    i.to_le_bytes()
621                };
622                out.extend_from_slice(&b);
623            }
624        }
625        ScalarArrayValue::U32(v) => {
626            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
627            for i in v {
628                let b = if is_be {
629                    i.to_be_bytes()
630                } else {
631                    i.to_le_bytes()
632                };
633                out.extend_from_slice(&b);
634            }
635        }
636        ScalarArrayValue::U64(v) => {
637            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
638            for i in v {
639                let b = if is_be {
640                    i.to_be_bytes()
641                } else {
642                    i.to_le_bytes()
643                };
644                out.extend_from_slice(&b);
645            }
646        }
647        ScalarArrayValue::F32(v) => {
648            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
649            for i in v {
650                let b = if is_be {
651                    i.to_be_bytes()
652                } else {
653                    i.to_le_bytes()
654                };
655                out.extend_from_slice(&b);
656            }
657        }
658        ScalarArrayValue::F64(v) => {
659            out.extend_from_slice(&encode_size_pvd(v.len(), is_be));
660            for i in v {
661                out.extend_from_slice(&encode_f64(*i, is_be));
662            }
663        }
664        ScalarArrayValue::Str(v) => {
665            out.extend_from_slice(&encode_string_array(v, is_be));
666        }
667    }
668    out
669}
670
671fn encode_nt_alarm(alarm: &NtAlarm, is_be: bool) -> Vec<u8> {
672    let mut out = Vec::new();
673    out.extend_from_slice(&encode_i32(alarm.severity, is_be));
674    out.extend_from_slice(&encode_i32(alarm.status, is_be));
675    out.extend_from_slice(&encode_string_pvd(&alarm.message, is_be));
676    out
677}
678
679fn encode_nt_timestamp(ts: &NtTimeStamp, is_be: bool) -> Vec<u8> {
680    let mut out = Vec::new();
681    out.extend_from_slice(&encode_i64(ts.seconds_past_epoch, is_be));
682    out.extend_from_slice(&encode_i32(ts.nanoseconds, is_be));
683    out.extend_from_slice(&encode_i32(ts.user_tag, is_be));
684    out
685}
686
687fn encode_nt_display(display: &NtDisplay, is_be: bool) -> Vec<u8> {
688    let mut out = Vec::new();
689    out.extend_from_slice(&encode_f64(display.limit_low, is_be));
690    out.extend_from_slice(&encode_f64(display.limit_high, is_be));
691    out.extend_from_slice(&encode_string_pvd(&display.description, is_be));
692    out.extend_from_slice(&encode_string_pvd(&display.units, is_be));
693    out.extend_from_slice(&encode_i32(display.precision, is_be));
694    out
695}
696
697pub fn nt_scalar_array_desc(value: &ScalarArrayValue) -> StructureDesc {
698    StructureDesc {
699        struct_id: Some("epics:nt/NTScalarArray:1.0".to_string()),
700        fields: vec![
701            FieldDesc {
702                name: "value".to_string(),
703                field_type: scalar_array_field_type(value),
704            },
705            FieldDesc {
706                name: "alarm".to_string(),
707                field_type: FieldType::Structure(alarm_desc()),
708            },
709            FieldDesc {
710                name: "timeStamp".to_string(),
711                field_type: FieldType::Structure(timestamp_desc()),
712            },
713            FieldDesc {
714                name: "display".to_string(),
715                field_type: FieldType::Structure(display_desc()),
716            },
717            FieldDesc {
718                name: "control".to_string(),
719                field_type: FieldType::Structure(StructureDesc {
720                    struct_id: Some("control_t".to_string()),
721                    fields: vec![
722                        FieldDesc {
723                            name: "limitLow".to_string(),
724                            field_type: FieldType::Scalar(TypeCode::Float64),
725                        },
726                        FieldDesc {
727                            name: "limitHigh".to_string(),
728                            field_type: FieldType::Scalar(TypeCode::Float64),
729                        },
730                        FieldDesc {
731                            name: "minStep".to_string(),
732                            field_type: FieldType::Scalar(TypeCode::Float64),
733                        },
734                    ],
735                }),
736            },
737        ],
738    }
739}
740
741pub fn encode_nt_scalar_array_full(nt: &NtScalarArray, is_be: bool) -> Vec<u8> {
742    let mut out = Vec::new();
743    out.extend_from_slice(&encode_scalar_array_value_pvd(&nt.value, is_be));
744    out.extend_from_slice(&encode_nt_alarm(&nt.alarm, is_be));
745    out.extend_from_slice(&encode_nt_timestamp(&nt.time_stamp, is_be));
746    out.extend_from_slice(&encode_nt_display(&nt.display, is_be));
747    out.extend_from_slice(&encode_f64(nt.control.limit_low, is_be));
748    out.extend_from_slice(&encode_f64(nt.control.limit_high, is_be));
749    out.extend_from_slice(&encode_f64(nt.control.min_step, is_be));
750    out
751}
752
753pub fn nt_table_desc(nt: &NtTable) -> StructureDesc {
754    let mut value_fields: Vec<FieldDesc> = Vec::new();
755    for col in &nt.columns {
756        value_fields.push(FieldDesc {
757            name: col.name.clone(),
758            field_type: scalar_array_field_type(&col.values),
759        });
760    }
761    StructureDesc {
762        struct_id: Some("epics:nt/NTTable:1.0".to_string()),
763        fields: vec![
764            FieldDesc {
765                name: "labels".to_string(),
766                field_type: FieldType::StringArray,
767            },
768            FieldDesc {
769                name: "value".to_string(),
770                field_type: FieldType::Structure(StructureDesc {
771                    struct_id: None,
772                    fields: value_fields,
773                }),
774            },
775        ],
776    }
777}
778
779pub fn encode_nt_table_full(nt: &NtTable, is_be: bool) -> Vec<u8> {
780    let mut out = Vec::new();
781    out.extend_from_slice(&encode_string_array(&nt.labels, is_be));
782    for NtTableColumn { values, .. } in &nt.columns {
783        out.extend_from_slice(&encode_scalar_array_value_pvd(values, is_be));
784    }
785    out
786}
787
788fn nt_ndarray_value_union_fields() -> Vec<FieldDesc> {
789    vec![
790        FieldDesc {
791            name: "booleanValue".to_string(),
792            field_type: FieldType::ScalarArray(TypeCode::Boolean),
793        },
794        FieldDesc {
795            name: "byteValue".to_string(),
796            field_type: FieldType::ScalarArray(TypeCode::Int8),
797        },
798        FieldDesc {
799            name: "shortValue".to_string(),
800            field_type: FieldType::ScalarArray(TypeCode::Int16),
801        },
802        FieldDesc {
803            name: "intValue".to_string(),
804            field_type: FieldType::ScalarArray(TypeCode::Int32),
805        },
806        FieldDesc {
807            name: "longValue".to_string(),
808            field_type: FieldType::ScalarArray(TypeCode::Int64),
809        },
810        FieldDesc {
811            name: "ubyteValue".to_string(),
812            field_type: FieldType::ScalarArray(TypeCode::UInt8),
813        },
814        FieldDesc {
815            name: "ushortValue".to_string(),
816            field_type: FieldType::ScalarArray(TypeCode::UInt16),
817        },
818        FieldDesc {
819            name: "uintValue".to_string(),
820            field_type: FieldType::ScalarArray(TypeCode::UInt32),
821        },
822        FieldDesc {
823            name: "ulongValue".to_string(),
824            field_type: FieldType::ScalarArray(TypeCode::UInt64),
825        },
826        FieldDesc {
827            name: "floatValue".to_string(),
828            field_type: FieldType::ScalarArray(TypeCode::Float32),
829        },
830        FieldDesc {
831            name: "doubleValue".to_string(),
832            field_type: FieldType::ScalarArray(TypeCode::Float64),
833        },
834        FieldDesc {
835            name: "stringValue".to_string(),
836            field_type: FieldType::StringArray,
837        },
838    ]
839}
840
841fn ndarray_union_index(value: &ScalarArrayValue) -> usize {
842    match value {
843        ScalarArrayValue::Bool(_) => 0,
844        ScalarArrayValue::I8(_) => 1,
845        ScalarArrayValue::I16(_) => 2,
846        ScalarArrayValue::I32(_) => 3,
847        ScalarArrayValue::I64(_) => 4,
848        ScalarArrayValue::U8(_) => 5,
849        ScalarArrayValue::U16(_) => 6,
850        ScalarArrayValue::U32(_) => 7,
851        ScalarArrayValue::U64(_) => 8,
852        ScalarArrayValue::F32(_) => 9,
853        ScalarArrayValue::F64(_) => 10,
854        ScalarArrayValue::Str(_) => 11,
855    }
856}
857
858fn encode_ndarray_union(value: &ScalarArrayValue, is_be: bool) -> Vec<u8> {
859    let mut out = Vec::new();
860    out.extend_from_slice(&encode_size_pvd(ndarray_union_index(value), is_be));
861    out.extend_from_slice(&encode_scalar_array_value_pvd(value, is_be));
862    out
863}
864
865fn encode_codec_parameters(
866    parameters: &std::collections::HashMap<String, String>,
867    is_be: bool,
868) -> Vec<u8> {
869    if parameters.is_empty() {
870        return vec![0xFF];
871    }
872    let mut out = Vec::new();
873    out.push(0x80);
874    let mut fields = Vec::new();
875    for key in parameters.keys() {
876        fields.push(FieldDesc {
877            name: key.clone(),
878            field_type: FieldType::String,
879        });
880    }
881    let desc = StructureDesc {
882        struct_id: None,
883        fields,
884    };
885    out.extend_from_slice(&encode_structure_desc(&desc, is_be));
886    for value in parameters.values() {
887        out.extend_from_slice(&encode_string_pvd(value, is_be));
888    }
889    out
890}
891
892pub fn nt_ndarray_desc_default() -> StructureDesc {
893    nt_ndarray_desc(&NtNdArray::empty())
894}
895
896pub fn nt_ndarray_desc(_nt: &NtNdArray) -> StructureDesc {
897    StructureDesc {
898        struct_id: Some("epics:nt/NTNDArray:1.0".to_string()),
899        fields: vec![
900            FieldDesc {
901                name: "value".to_string(),
902                field_type: FieldType::Union(nt_ndarray_value_union_fields()),
903            },
904            FieldDesc {
905                name: "codec".to_string(),
906                field_type: FieldType::Structure(StructureDesc {
907                    struct_id: Some("codec_t".to_string()),
908                    fields: vec![
909                        FieldDesc {
910                            name: "name".to_string(),
911                            field_type: FieldType::String,
912                        },
913                        FieldDesc {
914                            name: "parameters".to_string(),
915                            field_type: FieldType::Variant,
916                        },
917                    ],
918                }),
919            },
920            FieldDesc {
921                name: "compressedSize".to_string(),
922                field_type: FieldType::Scalar(TypeCode::Int64),
923            },
924            FieldDesc {
925                name: "uncompressedSize".to_string(),
926                field_type: FieldType::Scalar(TypeCode::Int64),
927            },
928            FieldDesc {
929                name: "dimension".to_string(),
930                field_type: FieldType::StructureArray(StructureDesc {
931                    struct_id: Some("dimension_t".to_string()),
932                    fields: vec![
933                        FieldDesc {
934                            name: "size".to_string(),
935                            field_type: FieldType::Scalar(TypeCode::Int32),
936                        },
937                        FieldDesc {
938                            name: "offset".to_string(),
939                            field_type: FieldType::Scalar(TypeCode::Int32),
940                        },
941                        FieldDesc {
942                            name: "fullSize".to_string(),
943                            field_type: FieldType::Scalar(TypeCode::Int32),
944                        },
945                        FieldDesc {
946                            name: "binning".to_string(),
947                            field_type: FieldType::Scalar(TypeCode::Int32),
948                        },
949                        FieldDesc {
950                            name: "reverse".to_string(),
951                            field_type: FieldType::Scalar(TypeCode::Boolean),
952                        },
953                    ],
954                }),
955            },
956            FieldDesc {
957                name: "uniqueId".to_string(),
958                field_type: FieldType::Scalar(TypeCode::Int32),
959            },
960            FieldDesc {
961                name: "dataTimeStamp".to_string(),
962                field_type: FieldType::Structure(timestamp_desc()),
963            },
964            FieldDesc {
965                name: "attribute".to_string(),
966                field_type: FieldType::StructureArray(StructureDesc {
967                    struct_id: Some("NTAttribute".to_string()),
968                    fields: vec![
969                        FieldDesc {
970                            name: "name".to_string(),
971                            field_type: FieldType::String,
972                        },
973                        FieldDesc {
974                            name: "value".to_string(),
975                            field_type: FieldType::Variant,
976                        },
977                        FieldDesc {
978                            name: "descriptor".to_string(),
979                            field_type: FieldType::String,
980                        },
981                        FieldDesc {
982                            name: "sourceType".to_string(),
983                            field_type: FieldType::Scalar(TypeCode::Int32),
984                        },
985                        FieldDesc {
986                            name: "source".to_string(),
987                            field_type: FieldType::String,
988                        },
989                    ],
990                }),
991            },
992            FieldDesc {
993                name: "descriptor".to_string(),
994                field_type: FieldType::String,
995            },
996            FieldDesc {
997                name: "alarm".to_string(),
998                field_type: FieldType::Structure(alarm_desc()),
999            },
1000            FieldDesc {
1001                name: "timeStamp".to_string(),
1002                field_type: FieldType::Structure(timestamp_desc()),
1003            },
1004            FieldDesc {
1005                name: "display".to_string(),
1006                field_type: FieldType::Structure(display_desc()),
1007            },
1008        ],
1009    }
1010}
1011
1012fn encode_attribute_variant(attr: &NtAttribute, is_be: bool) -> Vec<u8> {
1013    match &attr.value {
1014        ScalarValue::Bool(v) => {
1015            let mut out = vec![TypeCode::Boolean as u8];
1016            out.push(if *v { 1 } else { 0 });
1017            out
1018        }
1019        ScalarValue::I8(v) => {
1020            let mut out = vec![TypeCode::Int8 as u8];
1021            out.push(*v as u8);
1022            out
1023        }
1024        ScalarValue::I16(v) => {
1025            let mut out = vec![TypeCode::Int16 as u8];
1026            out.extend_from_slice(&if is_be {
1027                v.to_be_bytes().to_vec()
1028            } else {
1029                v.to_le_bytes().to_vec()
1030            });
1031            out
1032        }
1033        ScalarValue::I32(v) => {
1034            let mut out = vec![TypeCode::Int32 as u8];
1035            out.extend_from_slice(&encode_i32(*v, is_be));
1036            out
1037        }
1038        ScalarValue::I64(v) => {
1039            let mut out = vec![TypeCode::Int64 as u8];
1040            out.extend_from_slice(&encode_i64(*v, is_be));
1041            out
1042        }
1043        ScalarValue::U8(v) => {
1044            let mut out = vec![TypeCode::UInt8 as u8];
1045            out.push(*v);
1046            out
1047        }
1048        ScalarValue::U16(v) => {
1049            let mut out = vec![TypeCode::UInt16 as u8];
1050            out.extend_from_slice(&if is_be {
1051                v.to_be_bytes().to_vec()
1052            } else {
1053                v.to_le_bytes().to_vec()
1054            });
1055            out
1056        }
1057        ScalarValue::U32(v) => {
1058            let mut out = vec![TypeCode::UInt32 as u8];
1059            out.extend_from_slice(&if is_be {
1060                v.to_be_bytes().to_vec()
1061            } else {
1062                v.to_le_bytes().to_vec()
1063            });
1064            out
1065        }
1066        ScalarValue::U64(v) => {
1067            let mut out = vec![TypeCode::UInt64 as u8];
1068            out.extend_from_slice(&if is_be {
1069                v.to_be_bytes().to_vec()
1070            } else {
1071                v.to_le_bytes().to_vec()
1072            });
1073            out
1074        }
1075        ScalarValue::F32(v) => {
1076            let mut out = vec![TypeCode::Float32 as u8];
1077            out.extend_from_slice(&if is_be {
1078                v.to_be_bytes().to_vec()
1079            } else {
1080                v.to_le_bytes().to_vec()
1081            });
1082            out
1083        }
1084        ScalarValue::F64(v) => {
1085            let mut out = vec![TypeCode::Float64 as u8];
1086            out.extend_from_slice(&encode_f64(*v, is_be));
1087            out
1088        }
1089        ScalarValue::Str(v) => {
1090            let mut out = vec![TypeCode::String as u8];
1091            out.extend_from_slice(&encode_string_pvd(v, is_be));
1092            out
1093        }
1094    }
1095}
1096
1097pub fn encode_nt_ndarray_full(nt: &NtNdArray, is_be: bool) -> Vec<u8> {
1098    let mut out = Vec::new();
1099    out.extend_from_slice(&encode_ndarray_union(&nt.value, is_be));
1100    out.extend_from_slice(&encode_string_pvd(&nt.codec.name, is_be));
1101    out.extend_from_slice(&encode_codec_parameters(&nt.codec.parameters, is_be));
1102    out.extend_from_slice(&encode_i64(nt.compressed_size, is_be));
1103    out.extend_from_slice(&encode_i64(nt.uncompressed_size, is_be));
1104    out.extend_from_slice(&encode_size_pvd(nt.dimension.len(), is_be));
1105    for NdDimension {
1106        size,
1107        offset,
1108        full_size,
1109        binning,
1110        reverse,
1111    } in &nt.dimension
1112    {
1113        out.push(1); // non-null element indicator
1114        out.extend_from_slice(&encode_i32(*size, is_be));
1115        out.extend_from_slice(&encode_i32(*offset, is_be));
1116        out.extend_from_slice(&encode_i32(*full_size, is_be));
1117        out.extend_from_slice(&encode_i32(*binning, is_be));
1118        out.push(if *reverse { 1 } else { 0 });
1119    }
1120    out.extend_from_slice(&encode_i32(nt.unique_id, is_be));
1121    out.extend_from_slice(&encode_nt_timestamp(&nt.data_time_stamp, is_be));
1122    out.extend_from_slice(&encode_size_pvd(nt.attribute.len(), is_be));
1123    for attr in &nt.attribute {
1124        out.push(1); // non-null element indicator
1125        out.extend_from_slice(&encode_string_pvd(&attr.name, is_be));
1126        out.extend_from_slice(&encode_attribute_variant(attr, is_be));
1127        out.extend_from_slice(&encode_string_pvd(&attr.descriptor, is_be));
1128        out.extend_from_slice(&encode_i32(attr.source_type, is_be));
1129        out.extend_from_slice(&encode_string_pvd(&attr.source, is_be));
1130    }
1131    out.extend_from_slice(&encode_string_pvd(
1132        nt.descriptor.as_deref().unwrap_or(""),
1133        is_be,
1134    ));
1135    out.extend_from_slice(&encode_nt_alarm(
1136        nt.alarm.as_ref().unwrap_or(&NtAlarm::default()),
1137        is_be,
1138    ));
1139    out.extend_from_slice(&encode_nt_timestamp(
1140        nt.time_stamp.as_ref().unwrap_or(&NtTimeStamp::default()),
1141        is_be,
1142    ));
1143    out.extend_from_slice(&encode_nt_display(
1144        nt.display.as_ref().unwrap_or(&NtDisplay::default()),
1145        is_be,
1146    ));
1147    out
1148}
1149
1150// ---------------------------------------------------------------------------
1151// NTEnum descriptor & encoder
1152// ---------------------------------------------------------------------------
1153
1154pub fn nt_enum_desc() -> StructureDesc {
1155    StructureDesc {
1156        struct_id: Some("epics:nt/NTEnum:1.0".to_string()),
1157        fields: vec![
1158            FieldDesc {
1159                name: "value".to_string(),
1160                field_type: FieldType::Structure(StructureDesc {
1161                    struct_id: Some("enum_t".to_string()),
1162                    fields: vec![
1163                        FieldDesc {
1164                            name: "index".to_string(),
1165                            field_type: FieldType::Scalar(TypeCode::Int32),
1166                        },
1167                        FieldDesc {
1168                            name: "choices".to_string(),
1169                            field_type: FieldType::StringArray,
1170                        },
1171                    ],
1172                }),
1173            },
1174            FieldDesc {
1175                name: "alarm".to_string(),
1176                field_type: FieldType::Structure(alarm_desc()),
1177            },
1178            FieldDesc {
1179                name: "timeStamp".to_string(),
1180                field_type: FieldType::Structure(timestamp_desc()),
1181            },
1182        ],
1183    }
1184}
1185
1186pub fn encode_nt_enum_full(nt: &NtEnum, is_be: bool) -> Vec<u8> {
1187    let mut out = Vec::new();
1188    // value — enum_t { index, choices }
1189    out.extend_from_slice(&encode_enum(nt.index, &nt.choices, is_be));
1190    // alarm
1191    out.extend_from_slice(&encode_nt_alarm(&nt.alarm, is_be));
1192    // timeStamp
1193    out.extend_from_slice(&encode_nt_timestamp(&nt.time_stamp, is_be));
1194    out
1195}
1196
1197// ---------------------------------------------------------------------------
1198// PvValue (generic recursive) descriptor & encoder
1199// ---------------------------------------------------------------------------
1200
1201fn scalar_value_type_code(v: &ScalarValue) -> TypeCode {
1202    match v {
1203        ScalarValue::Bool(_) => TypeCode::Boolean,
1204        ScalarValue::I8(_) => TypeCode::Int8,
1205        ScalarValue::I16(_) => TypeCode::Int16,
1206        ScalarValue::I32(_) => TypeCode::Int32,
1207        ScalarValue::I64(_) => TypeCode::Int64,
1208        ScalarValue::U8(_) => TypeCode::UInt8,
1209        ScalarValue::U16(_) => TypeCode::UInt16,
1210        ScalarValue::U32(_) => TypeCode::UInt32,
1211        ScalarValue::U64(_) => TypeCode::UInt64,
1212        ScalarValue::F32(_) => TypeCode::Float32,
1213        ScalarValue::F64(_) => TypeCode::Float64,
1214        ScalarValue::Str(_) => TypeCode::String,
1215    }
1216}
1217
1218/// Build a [`StructureDesc`] from a [`PvValue::Structure`].
1219pub fn pv_value_desc(struct_id: &str, fields: &[(String, PvValue)]) -> StructureDesc {
1220    StructureDesc {
1221        struct_id: if struct_id.is_empty() {
1222            None
1223        } else {
1224            Some(struct_id.to_string())
1225        },
1226        fields: fields
1227            .iter()
1228            .map(|(name, val)| FieldDesc {
1229                name: name.clone(),
1230                field_type: pv_value_field_type(val),
1231            })
1232            .collect(),
1233    }
1234}
1235
1236fn pv_value_field_type(val: &PvValue) -> FieldType {
1237    match val {
1238        PvValue::Scalar(sv) => {
1239            if matches!(sv, ScalarValue::Str(_)) {
1240                FieldType::String
1241            } else {
1242                FieldType::Scalar(scalar_value_type_code(sv))
1243            }
1244        }
1245        PvValue::ScalarArray(sa) => scalar_array_field_type(sa),
1246        PvValue::Structure { struct_id, fields } => {
1247            FieldType::Structure(pv_value_desc(struct_id, fields))
1248        }
1249    }
1250}
1251
1252/// Encode a [`PvValue`] tree to PVA wire bytes (values only, no descriptor).
1253pub fn encode_pv_value(val: &PvValue, is_be: bool) -> Vec<u8> {
1254    match val {
1255        PvValue::Scalar(sv) => encode_scalar_value(sv, is_be),
1256        PvValue::ScalarArray(sa) => encode_scalar_array_value_pvd(sa, is_be),
1257        PvValue::Structure { fields, .. } => {
1258            let mut out = Vec::new();
1259            for (_, v) in fields {
1260                out.extend_from_slice(&encode_pv_value(v, is_be));
1261            }
1262            out
1263        }
1264    }
1265}
1266
1267pub fn nt_payload_desc(payload: &NtPayload) -> StructureDesc {
1268    match payload {
1269        NtPayload::Scalar(nt) => nt_scalar_desc(&nt.value),
1270        NtPayload::ScalarArray(nt) => nt_scalar_array_desc(&nt.value),
1271        NtPayload::Table(nt) => nt_table_desc(nt),
1272        NtPayload::NdArray(nt) => nt_ndarray_desc(nt),
1273        NtPayload::Enum(_) => nt_enum_desc(),
1274        NtPayload::Generic { struct_id, fields } => pv_value_desc(struct_id, fields),
1275    }
1276}
1277
1278pub fn encode_nt_payload_full(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1279    match payload {
1280        NtPayload::Scalar(nt) => encode_nt_scalar_full(nt, is_be),
1281        NtPayload::ScalarArray(nt) => encode_nt_scalar_array_full(nt, is_be),
1282        NtPayload::Table(nt) => encode_nt_table_full(nt, is_be),
1283        NtPayload::NdArray(nt) => encode_nt_ndarray_full(nt, is_be),
1284        NtPayload::Enum(nt) => encode_nt_enum_full(nt, is_be),
1285        NtPayload::Generic { fields, .. } => {
1286            let mut out = Vec::new();
1287            for (_, v) in fields {
1288                out.extend_from_slice(&encode_pv_value(v, is_be));
1289            }
1290            out
1291        }
1292    }
1293}
1294
1295pub fn encode_nt_payload_bitset(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1296    let desc = nt_payload_desc(payload);
1297    let mut out = Vec::new();
1298    out.extend_from_slice(&encode_structure_bitset(&desc, is_be));
1299    out.extend_from_slice(&encode_nt_payload_full(payload, is_be));
1300    out
1301}
1302
1303pub fn encode_nt_payload_bitset_parts(payload: &NtPayload, is_be: bool) -> (Vec<u8>, Vec<u8>) {
1304    let desc = nt_payload_desc(payload);
1305    (
1306        encode_structure_bitset(&desc, is_be),
1307        encode_nt_payload_full(payload, is_be),
1308    )
1309}
1310
1311// ---------------------------------------------------------------------------
1312// Generic DecodedValue → wire bytes encoder
1313// ---------------------------------------------------------------------------
1314
1315use crate::spvd_decode::DecodedValue;
1316
1317/// Encode a `DecodedValue` back to PVA wire bytes.
1318pub fn encode_decoded_value(val: &DecodedValue, is_be: bool) -> Vec<u8> {
1319    match val {
1320        DecodedValue::Null => Vec::new(),
1321        DecodedValue::Boolean(v) => vec![if *v { 1 } else { 0 }],
1322        DecodedValue::Int8(v) => vec![*v as u8],
1323        DecodedValue::Int16(v) => {
1324            if is_be {
1325                v.to_be_bytes().to_vec()
1326            } else {
1327                v.to_le_bytes().to_vec()
1328            }
1329        }
1330        DecodedValue::Int32(v) => encode_i32(*v, is_be),
1331        DecodedValue::Int64(v) => encode_i64(*v, is_be),
1332        DecodedValue::UInt8(v) => vec![*v],
1333        DecodedValue::UInt16(v) => {
1334            if is_be {
1335                v.to_be_bytes().to_vec()
1336            } else {
1337                v.to_le_bytes().to_vec()
1338            }
1339        }
1340        DecodedValue::UInt32(v) => {
1341            if is_be {
1342                v.to_be_bytes().to_vec()
1343            } else {
1344                v.to_le_bytes().to_vec()
1345            }
1346        }
1347        DecodedValue::UInt64(v) => {
1348            if is_be {
1349                v.to_be_bytes().to_vec()
1350            } else {
1351                v.to_le_bytes().to_vec()
1352            }
1353        }
1354        DecodedValue::Float32(v) => {
1355            if is_be {
1356                v.to_be_bytes().to_vec()
1357            } else {
1358                v.to_le_bytes().to_vec()
1359            }
1360        }
1361        DecodedValue::Float64(v) => encode_f64(*v, is_be),
1362        DecodedValue::String(v) => encode_string_pvd(v, is_be),
1363        DecodedValue::Array(arr) => {
1364            let mut out = encode_size_pvd(arr.len(), is_be);
1365            for item in arr {
1366                out.extend_from_slice(&encode_decoded_value(item, is_be));
1367            }
1368            out
1369        }
1370        DecodedValue::Structure(fields) => {
1371            let mut out = Vec::new();
1372            for (_name, value) in fields {
1373                out.extend_from_slice(&encode_decoded_value(value, is_be));
1374            }
1375            out
1376        }
1377        DecodedValue::Raw(data) => data.clone(),
1378    }
1379}
1380
1381// ---------------------------------------------------------------------------
1382// pvRequest parsing & descriptor filtering
1383// ---------------------------------------------------------------------------
1384
1385/// Parse a pvRequest structure from the INIT body bytes and return the list
1386/// of requested field paths.
1387///
1388/// Paths are returned as dot-separated strings (e.g. `"value"`,
1389/// `"alarm.severity"`). An empty inner `field {}` structure, or a body that
1390/// cannot be parsed, is reported as `None`, meaning "return all fields" (no
1391/// filtering).
1392///
1393/// A field whose inner pvRequest sub-structure is itself empty selects the
1394/// whole sub-tree rooted at that field (so `field(alarm)` → `["alarm"]`
1395/// selects the entire `alarm` structure). Non-empty sub-structures expand
1396/// into one entry per leaf path (so `field(alarm{severity}) →
1397/// ["alarm.severity"]`).
1398pub fn decode_pv_request_fields(body: &[u8], is_be: bool) -> Option<Vec<String>> {
1399    if body.is_empty() {
1400        return None;
1401    }
1402    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1403    let desc = decoder.parse_introspection(body)?;
1404    for field in &desc.fields {
1405        if field.name == "field" {
1406            if let FieldType::Structure(ref inner) = field.field_type {
1407                if inner.fields.is_empty() {
1408                    return None;
1409                }
1410                let mut paths = Vec::new();
1411                collect_pv_request_paths(inner, "", &mut paths);
1412                if paths.is_empty() {
1413                    return None;
1414                }
1415                return Some(paths);
1416            }
1417        }
1418    }
1419    None
1420}
1421
1422fn collect_pv_request_paths(desc: &StructureDesc, prefix: &str, out: &mut Vec<String>) {
1423    for field in &desc.fields {
1424        let joined = if prefix.is_empty() {
1425            field.name.clone()
1426        } else {
1427            format!("{}.{}", prefix, field.name)
1428        };
1429        match &field.field_type {
1430            FieldType::Structure(nested) if !nested.fields.is_empty() => {
1431                collect_pv_request_paths(nested, &joined, out);
1432            }
1433            _ => out.push(joined),
1434        }
1435    }
1436}
1437
1438/// Decode the `record._options` key/value pairs from a pvRequest body.
1439///
1440/// Returns `None` if the pvRequest does not include a `record._options`
1441/// substructure, or if the option values cannot be decoded as strings.
1442pub fn decode_pv_request_options(body: &[u8], is_be: bool) -> Option<Vec<(String, String)>> {
1443    if body.is_empty() {
1444        return None;
1445    }
1446    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1447    let desc = decoder.parse_introspection(body)?;
1448    let options_desc = desc.fields.iter().find_map(|f| {
1449        if f.name != "record" {
1450            return None;
1451        }
1452        if let FieldType::Structure(inner) = &f.field_type {
1453            inner.fields.iter().find_map(|g| {
1454                if g.name != "_options" {
1455                    return None;
1456                }
1457                if let FieldType::Structure(opts) = &g.field_type {
1458                    Some(opts.clone())
1459                } else {
1460                    None
1461                }
1462            })
1463        } else {
1464            None
1465        }
1466    })?;
1467
1468    // The pvRequest body is `0x80 <desc> <values>`. The `field` sub-tree
1469    // encodes empty structs only, so contributes no value bytes; the
1470    // option strings follow immediately after the descriptor.
1471    let desc_bytes = encode_structure_desc(&desc, is_be);
1472    let values_start = 1 + desc_bytes.len();
1473    if values_start > body.len() {
1474        return None;
1475    }
1476    let mut cursor = &body[values_start..];
1477    let mut out = Vec::with_capacity(options_desc.fields.len());
1478    for f in &options_desc.fields {
1479        if !matches!(f.field_type, FieldType::String) {
1480            return None;
1481        }
1482        let (s, consumed) = crate::epics_decode::decode_string(cursor, is_be)?;
1483        out.push((f.name.clone(), s));
1484        cursor = &cursor[consumed..];
1485    }
1486    Some(out)
1487}
1488
1489/// Filter a [`StructureDesc`] to include only the listed field paths.
1490///
1491/// Paths may be dot-separated to descend into nested structures (e.g.
1492/// `"alarm.severity"`). A bare name selects the entire sub-tree rooted at
1493/// that field. Unknown paths are silently dropped. If `requested` is empty
1494/// the original descriptor is returned unchanged.
1495pub fn filter_structure_desc(desc: &StructureDesc, requested: &[String]) -> StructureDesc {
1496    if requested.is_empty() {
1497        return desc.clone();
1498    }
1499    let tree = build_path_tree(requested);
1500    prune_structure(desc, &tree)
1501}
1502
1503#[derive(Default, Debug, Clone)]
1504struct PathNode {
1505    /// When true, the whole sub-tree rooted at this node is selected and
1506    /// `children` should be ignored.
1507    select_all: bool,
1508    /// Insertion-ordered children. Using a Vec of pairs rather than a map
1509    /// preserves the field order implied by the caller, which matters for
1510    /// the PVA wire format (introspection field order is significant).
1511    children: Vec<(String, PathNode)>,
1512}
1513
1514impl PathNode {
1515    fn child_mut(&mut self, name: &str) -> &mut PathNode {
1516        if let Some(idx) = self.children.iter().position(|(n, _)| n == name) {
1517            return &mut self.children[idx].1;
1518        }
1519        self.children.push((name.to_string(), PathNode::default()));
1520        &mut self.children.last_mut().unwrap().1
1521    }
1522
1523    fn child(&self, name: &str) -> Option<&PathNode> {
1524        self.children
1525            .iter()
1526            .find(|(n, _)| n == name)
1527            .map(|(_, c)| c)
1528    }
1529}
1530
1531fn build_path_tree(paths: &[String]) -> PathNode {
1532    let mut root = PathNode::default();
1533    for p in paths {
1534        let parts: Vec<&str> = p.split('.').filter(|s| !s.is_empty()).collect();
1535        if parts.is_empty() {
1536            continue;
1537        }
1538        let mut node = &mut root;
1539        for (i, part) in parts.iter().enumerate() {
1540            let is_last = i == parts.len() - 1;
1541            let child = node.child_mut(part);
1542            if is_last {
1543                child.select_all = true;
1544                child.children.clear();
1545            }
1546            node = child;
1547        }
1548    }
1549    root
1550}
1551
1552fn prune_structure(desc: &StructureDesc, node: &PathNode) -> StructureDesc {
1553    if node.select_all {
1554        return desc.clone();
1555    }
1556    let mut fields = Vec::new();
1557    for field in &desc.fields {
1558        let Some(child) = node.child(&field.name) else {
1559            continue;
1560        };
1561        if child.select_all {
1562            fields.push(field.clone());
1563            continue;
1564        }
1565        match &field.field_type {
1566            FieldType::Structure(inner) => {
1567                let pruned = prune_structure(inner, child);
1568                if !pruned.fields.is_empty() {
1569                    fields.push(FieldDesc {
1570                        name: field.name.clone(),
1571                        field_type: FieldType::Structure(pruned),
1572                    });
1573                }
1574            }
1575            FieldType::StructureArray(inner) => {
1576                // For structure arrays we can only narrow the element
1577                // descriptor; we never drop the array field itself when a
1578                // sub-path is requested.
1579                let pruned = prune_structure(inner, child);
1580                if !pruned.fields.is_empty() {
1581                    fields.push(FieldDesc {
1582                        name: field.name.clone(),
1583                        field_type: FieldType::StructureArray(pruned),
1584                    });
1585                }
1586            }
1587            _ => {
1588                // Leaf referenced with a deeper path – drop it (unresolved).
1589            }
1590        }
1591    }
1592    StructureDesc {
1593        struct_id: desc.struct_id.clone(),
1594        fields,
1595    }
1596}
1597
1598/// Encode only the fields of an [`NtPayload`] whose paths appear in
1599/// `filtered_desc`.  The bitset and value bytes are computed against the
1600/// filtered descriptor so that a client that received the filtered INIT
1601/// descriptor will decode them correctly.
1602///
1603/// Supports nested filtering (e.g. a filtered descriptor that only contains
1604/// `alarm.severity`).
1605pub fn encode_nt_payload_filtered(
1606    payload: &NtPayload,
1607    filtered_desc: &StructureDesc,
1608    is_be: bool,
1609) -> (Vec<u8>, Vec<u8>) {
1610    let bitset = encode_structure_bitset(filtered_desc, is_be);
1611    let values = encode_nt_payload_values_for_desc(payload, filtered_desc, is_be);
1612    (bitset, values)
1613}
1614
1615/// Encode the value bytes of an `NtPayload` projected onto a (possibly
1616/// narrowed) descriptor. Fields not represented in `desc` are omitted;
1617/// sub-structures are encoded recursively.
1618pub fn encode_nt_payload_values_for_desc(
1619    payload: &NtPayload,
1620    desc: &StructureDesc,
1621    is_be: bool,
1622) -> Vec<u8> {
1623    let full_desc = nt_payload_desc(payload);
1624    if structure_desc_equal(&full_desc, desc) {
1625        // Fast path: no narrowing.
1626        return encode_nt_payload_full(payload, is_be);
1627    }
1628    let decoded = decode_payload_to_structure(payload, is_be)
1629        .unwrap_or_else(|| DecodedValue::Structure(Vec::new()));
1630    encode_decoded_projected(&decoded, desc, is_be)
1631}
1632
1633fn structure_desc_equal(a: &StructureDesc, b: &StructureDesc) -> bool {
1634    if a.struct_id != b.struct_id {
1635        return false;
1636    }
1637    if a.fields.len() != b.fields.len() {
1638        return false;
1639    }
1640    a.fields
1641        .iter()
1642        .zip(&b.fields)
1643        .all(|(x, y)| x.name == y.name && field_type_equal(&x.field_type, &y.field_type))
1644}
1645
1646fn field_type_equal(a: &FieldType, b: &FieldType) -> bool {
1647    match (a, b) {
1648        (FieldType::Scalar(x), FieldType::Scalar(y)) => x == y,
1649        (FieldType::ScalarArray(x), FieldType::ScalarArray(y)) => x == y,
1650        (FieldType::String, FieldType::String) => true,
1651        (FieldType::StringArray, FieldType::StringArray) => true,
1652        (FieldType::Structure(x), FieldType::Structure(y)) => structure_desc_equal(x, y),
1653        (FieldType::StructureArray(x), FieldType::StructureArray(y)) => structure_desc_equal(x, y),
1654        (FieldType::Variant, FieldType::Variant) => true,
1655        (FieldType::VariantArray, FieldType::VariantArray) => true,
1656        (FieldType::BoundedString(x), FieldType::BoundedString(y)) => x == y,
1657        // Treat unions as equal only by count (rare in NT; fine for fast-path).
1658        (FieldType::Union(x), FieldType::Union(y)) => x.len() == y.len(),
1659        (FieldType::UnionArray(x), FieldType::UnionArray(y)) => x.len() == y.len(),
1660        _ => false,
1661    }
1662}
1663
1664/// Round-trip an NtPayload through its full descriptor to obtain a
1665/// `DecodedValue::Structure` we can project against a narrowed descriptor.
1666fn decode_payload_to_structure(payload: &NtPayload, is_be: bool) -> Option<DecodedValue> {
1667    let desc = nt_payload_desc(payload);
1668    let bytes = encode_nt_payload_full(payload, is_be);
1669    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1670    decoder.decode_structure(&bytes, &desc).map(|(v, _)| v)
1671}
1672
1673/// Re-encode a `DecodedValue::Structure` against a (possibly narrowed)
1674/// descriptor, omitting fields that are not present in the descriptor.
1675pub fn encode_decoded_projected(
1676    value: &DecodedValue,
1677    desc: &StructureDesc,
1678    is_be: bool,
1679) -> Vec<u8> {
1680    let DecodedValue::Structure(fields) = value else {
1681        // Fallback: not a structure – emit raw bytes.
1682        return encode_decoded_value(value, is_be);
1683    };
1684    let mut out = Vec::new();
1685    for target in &desc.fields {
1686        let Some((_, sub_value)) = fields.iter().find(|(n, _)| n == &target.name) else {
1687            continue;
1688        };
1689        match &target.field_type {
1690            FieldType::Structure(inner) => {
1691                out.extend_from_slice(&encode_decoded_projected(sub_value, inner, is_be));
1692            }
1693            _ => {
1694                out.extend_from_slice(&encode_decoded_value(sub_value, is_be));
1695            }
1696        }
1697    }
1698    out
1699}
1700
1701// ---------------------------------------------------------------------------
1702// Sparse delta encoding (Phase 3)
1703// ---------------------------------------------------------------------------
1704
1705/// Project an [`NtPayload`] onto `desc`, returning a [`DecodedValue::Structure`]
1706/// that contains only the fields represented in `desc`. Missing descriptor
1707/// fields are silently dropped.
1708fn project_payload_on_desc(payload: &NtPayload, desc: &StructureDesc, is_be: bool) -> DecodedValue {
1709    let decoded = decode_payload_to_structure(payload, is_be)
1710        .unwrap_or_else(|| DecodedValue::Structure(Vec::new()));
1711    project_decoded(&decoded, desc)
1712}
1713
1714fn project_decoded(value: &DecodedValue, desc: &StructureDesc) -> DecodedValue {
1715    let DecodedValue::Structure(fields) = value else {
1716        return value.clone();
1717    };
1718    let mut out: Vec<(String, DecodedValue)> = Vec::new();
1719    for target in &desc.fields {
1720        let Some((_, v)) = fields.iter().find(|(n, _)| n == &target.name) else {
1721            continue;
1722        };
1723        match &target.field_type {
1724            FieldType::Structure(inner) => {
1725                out.push((target.name.clone(), project_decoded(v, inner)));
1726            }
1727            _ => {
1728                out.push((target.name.clone(), v.clone()));
1729            }
1730        }
1731    }
1732    DecodedValue::Structure(out)
1733}
1734
1735/// Structural equality for [`DecodedValue`] with NaN treated as equal to NaN
1736/// (avoids spurious monitor flaps when a float field is NaN on both sides).
1737pub fn decoded_values_equal(a: &DecodedValue, b: &DecodedValue) -> bool {
1738    use DecodedValue::*;
1739    match (a, b) {
1740        (Null, Null) => true,
1741        (Boolean(x), Boolean(y)) => x == y,
1742        (Int8(x), Int8(y)) => x == y,
1743        (Int16(x), Int16(y)) => x == y,
1744        (Int32(x), Int32(y)) => x == y,
1745        (Int64(x), Int64(y)) => x == y,
1746        (UInt8(x), UInt8(y)) => x == y,
1747        (UInt16(x), UInt16(y)) => x == y,
1748        (UInt32(x), UInt32(y)) => x == y,
1749        (UInt64(x), UInt64(y)) => x == y,
1750        (Float32(x), Float32(y)) => x == y || (x.is_nan() && y.is_nan()),
1751        (Float64(x), Float64(y)) => x == y || (x.is_nan() && y.is_nan()),
1752        (String(x), String(y)) => x == y,
1753        (Raw(x), Raw(y)) => x == y,
1754        (Array(x), Array(y)) => {
1755            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| decoded_values_equal(a, b))
1756        }
1757        (Structure(x), Structure(y)) => {
1758            x.len() == y.len()
1759                && x.iter()
1760                    .zip(y)
1761                    .all(|((ln, lv), (rn, rv))| ln == rn && decoded_values_equal(lv, rv))
1762        }
1763        _ => false,
1764    }
1765}
1766
1767/// Walks `desc` in pre-order (matching the PVA wire convention that bit 0
1768/// represents the whole root structure and subsequent bits correspond to
1769/// fields in pre-order) and returns a per-bit flag vector marking leaves
1770/// whose value differs between `prev` and `next`.
1771///
1772/// Returns `None` if no leaves changed. Structure-type fields always have
1773/// their bit cleared — changes propagate to the descendants so a filtered
1774/// monitor client sees only the true differences.
1775pub fn compute_changed_bits(
1776    prev: &DecodedValue,
1777    next: &DecodedValue,
1778    desc: &StructureDesc,
1779) -> Option<Vec<bool>> {
1780    let total = 1 + spvd_count_structure_fields(desc);
1781    let mut bits = vec![false; total];
1782    let mut idx = 1usize;
1783    let any = fill_changed_bits(prev, next, desc, &mut bits, &mut idx);
1784    if any { Some(bits) } else { None }
1785}
1786
1787fn get_field_by_name<'a>(val: &'a DecodedValue, name: &str) -> Option<&'a DecodedValue> {
1788    match val {
1789        DecodedValue::Structure(f) => f.iter().find(|(n, _)| n == name).map(|(_, v)| v),
1790        _ => None,
1791    }
1792}
1793
1794fn fill_changed_bits(
1795    prev: &DecodedValue,
1796    next: &DecodedValue,
1797    desc: &StructureDesc,
1798    bits: &mut [bool],
1799    idx: &mut usize,
1800) -> bool {
1801    let mut any = false;
1802    for field in &desc.fields {
1803        let this = *idx;
1804        *idx += 1;
1805        let p = get_field_by_name(prev, &field.name);
1806        let n = get_field_by_name(next, &field.name);
1807        match &field.field_type {
1808            FieldType::Structure(inner) => {
1809                let empty = DecodedValue::Structure(Vec::new());
1810                let pv = p.unwrap_or(&empty);
1811                let nv = n.unwrap_or(&empty);
1812                if fill_changed_bits(pv, nv, inner, bits, idx) {
1813                    any = true;
1814                }
1815            }
1816            _ => {
1817                let changed = match (p, n) {
1818                    (Some(a), Some(b)) => !decoded_values_equal(a, b),
1819                    (Some(_), None) | (None, Some(_)) => true,
1820                    (None, None) => false,
1821                };
1822                if changed {
1823                    bits[this] = true;
1824                    any = true;
1825                }
1826            }
1827        }
1828    }
1829    any
1830}
1831
1832fn encode_values_for_bits(
1833    value: &DecodedValue,
1834    desc: &StructureDesc,
1835    bits: &[bool],
1836    idx: &mut usize,
1837    is_be: bool,
1838    out: &mut Vec<u8>,
1839) {
1840    for field in &desc.fields {
1841        let this = *idx;
1842        *idx += 1;
1843        let sub = get_field_by_name(value, &field.name);
1844        match &field.field_type {
1845            FieldType::Structure(inner) => {
1846                let empty = DecodedValue::Structure(Vec::new());
1847                let v = sub.unwrap_or(&empty);
1848                encode_values_for_bits(v, inner, bits, idx, is_be, out);
1849            }
1850            _ => {
1851                if bits[this] {
1852                    if let Some(v) = sub {
1853                        out.extend_from_slice(&encode_decoded_value(v, is_be));
1854                    }
1855                }
1856            }
1857        }
1858    }
1859}
1860
1861fn encode_bitset_from_flags(bits: &[bool], is_be: bool) -> Vec<u8> {
1862    let bitset_size = (bits.len() + 7) / 8;
1863    let mut bitset = vec![0u8; bitset_size];
1864    for (i, b) in bits.iter().enumerate() {
1865        if *b {
1866            bitset[i / 8] |= 1 << (i % 8);
1867        }
1868    }
1869    let mut out = Vec::new();
1870    out.extend_from_slice(&encode_size_pvd(bitset_size, is_be));
1871    out.extend_from_slice(&bitset);
1872    out
1873}
1874
1875/// Encode a sparse monitor-data delta between `prev` and `next` projected onto
1876/// `filtered_desc`. Returns `None` if nothing changed in the filtered view
1877/// (caller should suppress the update). Otherwise returns `(bitset, values)`
1878/// with only the changed leaves marked and encoded.
1879pub fn encode_nt_payload_delta(
1880    prev: &NtPayload,
1881    next: &NtPayload,
1882    filtered_desc: &StructureDesc,
1883    is_be: bool,
1884) -> Option<(Vec<u8>, Vec<u8>)> {
1885    let prev_proj = project_payload_on_desc(prev, filtered_desc, is_be);
1886    let next_proj = project_payload_on_desc(next, filtered_desc, is_be);
1887    let bits = compute_changed_bits(&prev_proj, &next_proj, filtered_desc)?;
1888    let bitset = encode_bitset_from_flags(&bits, is_be);
1889    let mut values = Vec::new();
1890    let mut idx = 1usize;
1891    encode_values_for_bits(
1892        &next_proj,
1893        filtered_desc,
1894        &bits,
1895        &mut idx,
1896        is_be,
1897        &mut values,
1898    );
1899    Some((bitset, values))
1900}
1901
1902fn spvd_count_structure_fields(desc: &StructureDesc) -> usize {
1903    let mut count = 0;
1904    for field in &desc.fields {
1905        count += 1;
1906        if let FieldType::Structure(inner) = &field.field_type {
1907            count += spvd_count_structure_fields(inner);
1908        }
1909    }
1910    count
1911}
1912
1913// ---------------------------------------------------------------------------
1914// pvRequest builder
1915// ---------------------------------------------------------------------------
1916// ---------------------------------------------------------------------------
1917// pvRequest builder
1918// ---------------------------------------------------------------------------
1919
1920/// Build a pvRequest structure for the given field paths.
1921///
1922/// Each entry may be a simple top-level name (e.g. `"value"`) or a
1923/// dot-separated nested path (e.g. `"alarm.severity"`,
1924/// `"timeStamp.secondsPastEpoch"`).
1925///
1926/// A bare name selects the entire sub-tree rooted at that field. Nested
1927/// paths produce the corresponding nested sub-structure in the pvRequest so
1928/// that a PVA server can filter down to the requested leaves.
1929///
1930/// Examples:
1931/// - `encode_pv_request(&["value", "alarm", "timeStamp"], false)` →
1932///   `field(value,alarm,timeStamp)`
1933/// - `encode_pv_request(&["alarm.severity"], false)` →
1934///   `field(alarm{severity})`
1935///
1936/// The output is the *full* type-described pvRequest structure: a `0x80`
1937/// tag followed by the structure descriptor and empty-struct field values.
1938pub fn encode_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
1939    encode_pv_request_with_options(fields, &[], is_be)
1940}
1941
1942/// Build a pvRequest structure with extra `record._options` key/value pairs.
1943///
1944/// `options` is an ordered list of `(name, value)` pairs (both strings) that
1945/// are encoded as `structure record { structure _options { string name; ... } }`
1946/// alongside the usual `field(...)` selector. This is the standard PVAccess
1947/// mechanism for requesting transport options such as
1948/// `pipeline=true,queueSize=N` on a monitor.
1949///
1950/// Empty `options` is equivalent to [`encode_pv_request`].
1951pub fn encode_pv_request_with_options(
1952    fields: &[&str],
1953    options: &[(&str, &str)],
1954    is_be: bool,
1955) -> Vec<u8> {
1956    let tree = build_path_tree_from_strs(fields);
1957    let inner_fields = path_tree_to_field_descs(&tree);
1958
1959    let field_desc = StructureDesc {
1960        struct_id: None,
1961        fields: inner_fields,
1962    };
1963
1964    let mut top_fields = vec![FieldDesc {
1965        name: "field".to_string(),
1966        field_type: FieldType::Structure(field_desc),
1967    }];
1968
1969    if !options.is_empty() {
1970        let options_desc = StructureDesc {
1971            struct_id: None,
1972            fields: options
1973                .iter()
1974                .map(|(k, _)| FieldDesc {
1975                    name: (*k).to_string(),
1976                    field_type: FieldType::String,
1977                })
1978                .collect(),
1979        };
1980        let record_desc = StructureDesc {
1981            struct_id: None,
1982            fields: vec![FieldDesc {
1983                name: "_options".to_string(),
1984                field_type: FieldType::Structure(options_desc),
1985            }],
1986        };
1987        top_fields.push(FieldDesc {
1988            name: "record".to_string(),
1989            field_type: FieldType::Structure(record_desc),
1990        });
1991    }
1992
1993    let pv_request_desc = StructureDesc {
1994        struct_id: None,
1995        fields: top_fields,
1996    };
1997
1998    let mut out = Vec::new();
1999    out.push(0x80); // structure tag
2000    out.extend_from_slice(&encode_structure_desc(&pv_request_desc, is_be));
2001    // Values: the `field` sub-tree is entirely empty structs (no leaves), so
2002    // it contributes no bytes. If options are present, append the string
2003    // values for record._options in declared order.
2004    for (_, v) in options {
2005        out.extend_from_slice(&encode_string_pvd(v, is_be));
2006    }
2007    out
2008}
2009
2010fn build_path_tree_from_strs(paths: &[&str]) -> PathNode {
2011    let owned: Vec<String> = paths.iter().map(|s| (*s).to_string()).collect();
2012    build_path_tree(&owned)
2013}
2014
2015fn path_tree_to_field_descs(node: &PathNode) -> Vec<FieldDesc> {
2016    node.children
2017        .iter()
2018        .map(|(name, child)| {
2019            let nested_fields = if child.select_all {
2020                Vec::new()
2021            } else {
2022                path_tree_to_field_descs(child)
2023            };
2024            FieldDesc {
2025                name: name.clone(),
2026                field_type: FieldType::Structure(StructureDesc {
2027                    struct_id: None,
2028                    fields: nested_fields,
2029                }),
2030            }
2031        })
2032        .collect()
2033}
2034
2035#[cfg(test)]
2036mod tests {
2037    use super::*;
2038    use crate::spvd_decode::PvdDecoder;
2039
2040    #[test]
2041    fn nt_scalar_roundtrip() {
2042        let nt = NtScalar::from_value(ScalarValue::F64(12.5));
2043        let desc = nt_scalar_desc(&nt.value);
2044        let desc_bytes = encode_structure_desc(&desc, false);
2045        let mut pvd = Vec::new();
2046        pvd.push(0x80);
2047        pvd.extend_from_slice(&desc_bytes);
2048        pvd.extend_from_slice(&encode_nt_scalar_full(&nt, false));
2049
2050        let decoder = PvdDecoder::new(false);
2051        let parsed_desc = decoder.parse_introspection(&pvd).expect("desc");
2052        let (_, consumed) = decoder
2053            .decode_structure(&pvd[1 + desc_bytes.len()..], &parsed_desc)
2054            .expect("value");
2055        assert!(consumed > 0);
2056    }
2057
2058    #[test]
2059    fn nt_ndarray_roundtrip() {
2060        use spvirit_types::{
2061            NdCodec, NdDimension, NtAlarm, NtNdArray, NtTimeStamp, ScalarArrayValue,
2062        };
2063        use std::collections::HashMap;
2064
2065        let nt = NtNdArray {
2066            value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
2067            codec: NdCodec {
2068                name: String::new(),
2069                parameters: HashMap::new(),
2070            },
2071            compressed_size: 4,
2072            uncompressed_size: 4,
2073            dimension: vec![NdDimension {
2074                size: 2,
2075                offset: 0,
2076                full_size: 2,
2077                binning: 1,
2078                reverse: false,
2079            }],
2080            unique_id: 42,
2081            data_time_stamp: NtTimeStamp {
2082                seconds_past_epoch: 1000,
2083                nanoseconds: 500,
2084                user_tag: 0,
2085            },
2086            attribute: Vec::new(),
2087            descriptor: Some("test".to_string()),
2088            alarm: Some(NtAlarm::default()),
2089            time_stamp: Some(NtTimeStamp::default()),
2090            display: None,
2091        };
2092
2093        let desc = nt_ndarray_desc(&nt);
2094        let desc_bytes = encode_structure_desc(&desc, false);
2095        let data_bytes = encode_nt_ndarray_full(&nt, false);
2096
2097        // Build complete PVD: type_tag + desc + data
2098        let mut pvd = Vec::new();
2099        pvd.push(0x80);
2100        pvd.extend_from_slice(&desc_bytes);
2101        pvd.extend_from_slice(&data_bytes);
2102
2103        let decoder = PvdDecoder::new(false);
2104        let parsed_desc = decoder
2105            .parse_introspection(&pvd)
2106            .expect("desc parse failed");
2107        let data_start = 1 + desc_bytes.len();
2108        let (_decoded, consumed) = decoder
2109            .decode_structure(&pvd[data_start..], &parsed_desc)
2110            .expect("data decode failed");
2111        assert!(consumed > 0, "consumed should be > 0");
2112        assert_eq!(
2113            consumed,
2114            data_bytes.len(),
2115            "consumed should match data_bytes.len()"
2116        );
2117    }
2118
2119    #[test]
2120    fn pv_request_flat_roundtrip() {
2121        for is_be in [false, true] {
2122            let body = encode_pv_request(&["value", "alarm", "timeStamp"], is_be);
2123            let fields = decode_pv_request_fields(&body, is_be).expect("fields");
2124            assert_eq!(fields, vec!["value", "alarm", "timeStamp"]);
2125        }
2126    }
2127
2128    #[test]
2129    fn pv_request_nested_roundtrip() {
2130        let body = encode_pv_request(&["alarm.severity", "timeStamp.secondsPastEpoch"], false);
2131        let fields = decode_pv_request_fields(&body, false).expect("fields");
2132        assert_eq!(
2133            fields,
2134            vec![
2135                "alarm.severity".to_string(),
2136                "timeStamp.secondsPastEpoch".to_string()
2137            ]
2138        );
2139    }
2140
2141    #[test]
2142    fn pv_request_whole_subtree_beats_leaf() {
2143        // Requesting both "alarm" and "alarm.severity" should collapse to the
2144        // whole-subtree selection.
2145        let body = encode_pv_request(&["alarm.severity", "alarm"], false);
2146        let fields = decode_pv_request_fields(&body, false).expect("fields");
2147        assert_eq!(fields, vec!["alarm".to_string()]);
2148    }
2149
2150    #[test]
2151    fn pv_request_with_pipeline_options_roundtrip() {
2152        for is_be in [false, true] {
2153            let body = encode_pv_request_with_options(
2154                &["value", "alarm"],
2155                &[("pipeline", "true"), ("queueSize", "4")],
2156                is_be,
2157            );
2158            // Field selectors still parse correctly.
2159            let fields = decode_pv_request_fields(&body, is_be).expect("fields");
2160            assert_eq!(fields, vec!["value".to_string(), "alarm".to_string()]);
2161            // Options round-trip.
2162            let opts = decode_pv_request_options(&body, is_be).expect("opts");
2163            assert_eq!(
2164                opts,
2165                vec![
2166                    ("pipeline".to_string(), "true".to_string()),
2167                    ("queueSize".to_string(), "4".to_string()),
2168                ]
2169            );
2170        }
2171    }
2172
2173    #[test]
2174    fn pv_request_without_options_has_no_record() {
2175        let body = encode_pv_request(&["value"], false);
2176        assert!(decode_pv_request_options(&body, false).is_none());
2177    }
2178
2179    #[test]
2180    fn pv_request_empty_body_none() {
2181        assert!(decode_pv_request_fields(&[], false).is_none());
2182    }
2183
2184    #[test]
2185    fn filter_structure_desc_nested() {
2186        let alarm = StructureDesc {
2187            struct_id: Some("alarm_t".to_string()),
2188            fields: vec![
2189                FieldDesc {
2190                    name: "severity".into(),
2191                    field_type: FieldType::Scalar(TypeCode::Int32),
2192                },
2193                FieldDesc {
2194                    name: "status".into(),
2195                    field_type: FieldType::Scalar(TypeCode::Int32),
2196                },
2197                FieldDesc {
2198                    name: "message".into(),
2199                    field_type: FieldType::String,
2200                },
2201            ],
2202        };
2203        let desc = StructureDesc {
2204            struct_id: Some("epics:nt/NTScalar:1.0".into()),
2205            fields: vec![
2206                FieldDesc {
2207                    name: "value".into(),
2208                    field_type: FieldType::Scalar(TypeCode::Float64),
2209                },
2210                FieldDesc {
2211                    name: "alarm".into(),
2212                    field_type: FieldType::Structure(alarm.clone()),
2213                },
2214            ],
2215        };
2216
2217        let pruned = filter_structure_desc(&desc, &["alarm.severity".to_string()]);
2218        assert_eq!(pruned.fields.len(), 1);
2219        assert_eq!(pruned.fields[0].name, "alarm");
2220        match &pruned.fields[0].field_type {
2221            FieldType::Structure(inner) => {
2222                assert_eq!(inner.fields.len(), 1);
2223                assert_eq!(inner.fields[0].name, "severity");
2224            }
2225            other => panic!("expected Structure, got {:?}", other),
2226        }
2227
2228        // Whole-subtree selection preserves the full alarm.
2229        let pruned_all = filter_structure_desc(&desc, &["alarm".to_string()]);
2230        match &pruned_all.fields[0].field_type {
2231            FieldType::Structure(inner) => assert_eq!(inner.fields.len(), 3),
2232            other => panic!("expected Structure, got {:?}", other),
2233        }
2234
2235        // Unknown paths are silently dropped.
2236        let pruned_unknown = filter_structure_desc(&desc, &["nope".into(), "alarm.missing".into()]);
2237        assert!(pruned_unknown.fields.is_empty());
2238    }
2239
2240    #[test]
2241    fn filtered_monitor_round_trip_nested() {
2242        use crate::spvd_decode::PvdDecoder;
2243        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2244
2245        let mut nt = NtScalar::from_value(ScalarValue::F64(42.0));
2246        nt.alarm_severity = 2;
2247        nt.alarm_status = 7;
2248        nt.alarm_message = "hi".into();
2249        let payload = NtPayload::Scalar(nt);
2250
2251        // Client sends field(alarm.severity).
2252        let full_desc = nt_payload_desc(&payload);
2253        let paths = vec!["alarm.severity".to_string()];
2254        let filtered = filter_structure_desc(&full_desc, &paths);
2255        let (bitset, values) = encode_nt_payload_filtered(&payload, &filtered, false);
2256
2257        // Round-trip the filtered body using the filtered descriptor.
2258        let decoder = PvdDecoder::new(false);
2259        let mut body = bitset.clone();
2260        body.extend_from_slice(&values);
2261        let (decoded, _) = decoder
2262            .decode_structure_with_bitset(&body, &filtered)
2263            .expect("decode filtered");
2264
2265        let DecodedValue::Structure(fields) = decoded else {
2266            panic!("expected structure");
2267        };
2268        assert_eq!(fields.len(), 1);
2269        assert_eq!(fields[0].0, "alarm");
2270        match &fields[0].1 {
2271            DecodedValue::Structure(inner) => {
2272                assert_eq!(inner.len(), 1);
2273                assert_eq!(inner[0].0, "severity");
2274                assert!(matches!(inner[0].1, DecodedValue::Int32(2)));
2275            }
2276            other => panic!("expected Structure, got {:?}", other),
2277        }
2278
2279        // Verify that the filtered payload is genuinely smaller than an
2280        // unfiltered one (proves we're not emitting status/message bytes).
2281        let full_body_len = encode_nt_payload_full(&payload, false).len();
2282        assert!(values.len() < full_body_len);
2283    }
2284
2285    #[test]
2286    fn delta_returns_none_when_nothing_changed() {
2287        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2288        let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
2289        a.alarm_severity = 1;
2290        let p1 = NtPayload::Scalar(a.clone());
2291        let p2 = NtPayload::Scalar(a);
2292        let desc = filter_structure_desc(&nt_payload_desc(&p1), &["alarm.severity".to_string()]);
2293        assert!(encode_nt_payload_delta(&p1, &p2, &desc, false).is_none());
2294    }
2295
2296    #[test]
2297    fn delta_marks_only_changed_leaf() {
2298        use crate::spvd_decode::PvdDecoder;
2299        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2300
2301        let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
2302        a.alarm_severity = 1;
2303        a.alarm_status = 0;
2304        a.alarm_message = "ok".into();
2305        let mut b = a.clone();
2306        b.alarm_severity = 2; // only this leaf changes
2307        let p1 = NtPayload::Scalar(a);
2308        let p2 = NtPayload::Scalar(b);
2309        let desc = filter_structure_desc(
2310            &nt_payload_desc(&p1),
2311            &[
2312                "alarm.severity".to_string(),
2313                "alarm.status".to_string(),
2314                "alarm.message".to_string(),
2315            ],
2316        );
2317
2318        let (bitset, values) = encode_nt_payload_delta(&p1, &p2, &desc, false)
2319            .expect("delta must produce a frame when a leaf changed");
2320
2321        // Only one leaf bit set (severity). Bit layout for filtered_desc:
2322        //   bit 0 = root, bit 1 = alarm struct, bit 2 = severity,
2323        //   bit 3 = status, bit 4 = message.
2324        // => 5 bits → 1-byte bitset payload. The first byte is the size
2325        // prefix (1), followed by the bitset byte.
2326        assert_eq!(bitset[0], 1u8, "size prefix");
2327        let b0 = bitset[1];
2328        assert_eq!(b0 & 0x01, 0, "root bit must be clear");
2329        assert_eq!(b0 & 0x02, 0, "alarm struct bit must be clear");
2330        assert_eq!(b0 & 0x04, 0x04, "severity bit must be set");
2331        assert_eq!(b0 & 0x08, 0, "status bit must be clear");
2332        assert_eq!(b0 & 0x10, 0, "message bit must be clear");
2333
2334        // values should contain exactly one i32 (4 bytes).
2335        assert_eq!(values.len(), 4);
2336
2337        // Round-trip through the decoder and check severity.
2338        let decoder = PvdDecoder::new(false);
2339        let mut body = bitset.clone();
2340        body.extend_from_slice(&values);
2341        let (decoded, _) = decoder
2342            .decode_structure_with_bitset(&body, &desc)
2343            .expect("decode delta");
2344        let DecodedValue::Structure(fields) = decoded else {
2345            panic!("expected struct")
2346        };
2347        assert_eq!(fields.len(), 1);
2348        assert_eq!(fields[0].0, "alarm");
2349        match &fields[0].1 {
2350            DecodedValue::Structure(inner) => {
2351                assert_eq!(inner.len(), 1);
2352                assert_eq!(inner[0].0, "severity");
2353                assert!(matches!(inner[0].1, DecodedValue::Int32(2)));
2354            }
2355            other => panic!("expected struct got {:?}", other),
2356        }
2357    }
2358
2359    #[test]
2360    fn decoded_values_equal_treats_nan_as_equal() {
2361        let a = DecodedValue::Float64(f64::NAN);
2362        let b = DecodedValue::Float64(f64::NAN);
2363        assert!(decoded_values_equal(&a, &b));
2364        let c = DecodedValue::Float32(f32::NAN);
2365        let d = DecodedValue::Float32(f32::NAN);
2366        assert!(decoded_values_equal(&c, &d));
2367        // But different concrete values are still different.
2368        assert!(!decoded_values_equal(
2369            &DecodedValue::Float64(1.0),
2370            &DecodedValue::Float64(2.0)
2371        ));
2372    }
2373}