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