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            FieldDesc {
787                name: "descriptor".to_string(),
788                field_type: FieldType::String,
789            },
790            FieldDesc {
791                name: "alarm".to_string(),
792                field_type: FieldType::Structure(alarm_desc()),
793            },
794            // Required by archiving clients: the EPICS Archiver Appliance
795            // refuses (or NPEs on) structures without a top-level timeStamp.
796            FieldDesc {
797                name: "timeStamp".to_string(),
798                field_type: FieldType::Structure(timestamp_desc()),
799            },
800        ],
801    }
802}
803
804pub fn encode_nt_table_full(nt: &NtTable, is_be: bool) -> Vec<u8> {
805    let mut out = Vec::new();
806    out.extend_from_slice(&encode_string_array(&nt.labels, is_be));
807    for NtTableColumn { values, .. } in &nt.columns {
808        out.extend_from_slice(&encode_scalar_array_value_pvd(values, is_be));
809    }
810    out.extend_from_slice(&encode_string_pvd(
811        nt.descriptor.as_deref().unwrap_or(""),
812        is_be,
813    ));
814    out.extend_from_slice(&encode_nt_alarm(
815        nt.alarm.as_ref().unwrap_or(&NtAlarm::default()),
816        is_be,
817    ));
818    out.extend_from_slice(&encode_nt_timestamp(
819        nt.time_stamp.as_ref().unwrap_or(&NtTimeStamp::default()),
820        is_be,
821    ));
822    out
823}
824
825fn nt_ndarray_value_union_fields() -> Vec<FieldDesc> {
826    vec![
827        FieldDesc {
828            name: "booleanValue".to_string(),
829            field_type: FieldType::ScalarArray(TypeCode::Boolean),
830        },
831        FieldDesc {
832            name: "byteValue".to_string(),
833            field_type: FieldType::ScalarArray(TypeCode::Int8),
834        },
835        FieldDesc {
836            name: "shortValue".to_string(),
837            field_type: FieldType::ScalarArray(TypeCode::Int16),
838        },
839        FieldDesc {
840            name: "intValue".to_string(),
841            field_type: FieldType::ScalarArray(TypeCode::Int32),
842        },
843        FieldDesc {
844            name: "longValue".to_string(),
845            field_type: FieldType::ScalarArray(TypeCode::Int64),
846        },
847        FieldDesc {
848            name: "ubyteValue".to_string(),
849            field_type: FieldType::ScalarArray(TypeCode::UInt8),
850        },
851        FieldDesc {
852            name: "ushortValue".to_string(),
853            field_type: FieldType::ScalarArray(TypeCode::UInt16),
854        },
855        FieldDesc {
856            name: "uintValue".to_string(),
857            field_type: FieldType::ScalarArray(TypeCode::UInt32),
858        },
859        FieldDesc {
860            name: "ulongValue".to_string(),
861            field_type: FieldType::ScalarArray(TypeCode::UInt64),
862        },
863        FieldDesc {
864            name: "floatValue".to_string(),
865            field_type: FieldType::ScalarArray(TypeCode::Float32),
866        },
867        FieldDesc {
868            name: "doubleValue".to_string(),
869            field_type: FieldType::ScalarArray(TypeCode::Float64),
870        },
871        FieldDesc {
872            name: "stringValue".to_string(),
873            field_type: FieldType::StringArray,
874        },
875    ]
876}
877
878fn ndarray_union_index(value: &ScalarArrayValue) -> usize {
879    match value {
880        ScalarArrayValue::Bool(_) => 0,
881        ScalarArrayValue::I8(_) => 1,
882        ScalarArrayValue::I16(_) => 2,
883        ScalarArrayValue::I32(_) => 3,
884        ScalarArrayValue::I64(_) => 4,
885        ScalarArrayValue::U8(_) => 5,
886        ScalarArrayValue::U16(_) => 6,
887        ScalarArrayValue::U32(_) => 7,
888        ScalarArrayValue::U64(_) => 8,
889        ScalarArrayValue::F32(_) => 9,
890        ScalarArrayValue::F64(_) => 10,
891        ScalarArrayValue::Str(_) => 11,
892    }
893}
894
895fn encode_ndarray_union(value: &ScalarArrayValue, is_be: bool) -> Vec<u8> {
896    let mut out = Vec::new();
897    out.extend_from_slice(&encode_size_pvd(ndarray_union_index(value), is_be));
898    out.extend_from_slice(&encode_scalar_array_value_pvd(value, is_be));
899    out
900}
901
902fn encode_codec_parameters(
903    parameters: &std::collections::HashMap<String, String>,
904    is_be: bool,
905) -> Vec<u8> {
906    if parameters.is_empty() {
907        return vec![0xFF];
908    }
909    let mut out = Vec::new();
910    out.push(0x80);
911    let mut fields = Vec::new();
912    for key in parameters.keys() {
913        fields.push(FieldDesc {
914            name: key.clone(),
915            field_type: FieldType::String,
916        });
917    }
918    let desc = StructureDesc {
919        struct_id: None,
920        fields,
921    };
922    out.extend_from_slice(&encode_structure_desc(&desc, is_be));
923    for value in parameters.values() {
924        out.extend_from_slice(&encode_string_pvd(value, is_be));
925    }
926    out
927}
928
929pub fn nt_ndarray_desc_default() -> StructureDesc {
930    nt_ndarray_desc(&NtNdArray::empty())
931}
932
933pub fn nt_ndarray_desc(_nt: &NtNdArray) -> StructureDesc {
934    StructureDesc {
935        struct_id: Some("epics:nt/NTNDArray:1.0".to_string()),
936        fields: vec![
937            FieldDesc {
938                name: "value".to_string(),
939                field_type: FieldType::Union(nt_ndarray_value_union_fields()),
940            },
941            FieldDesc {
942                name: "codec".to_string(),
943                field_type: FieldType::Structure(StructureDesc {
944                    struct_id: Some("codec_t".to_string()),
945                    fields: vec![
946                        FieldDesc {
947                            name: "name".to_string(),
948                            field_type: FieldType::String,
949                        },
950                        FieldDesc {
951                            name: "parameters".to_string(),
952                            field_type: FieldType::Variant,
953                        },
954                    ],
955                }),
956            },
957            FieldDesc {
958                name: "compressedSize".to_string(),
959                field_type: FieldType::Scalar(TypeCode::Int64),
960            },
961            FieldDesc {
962                name: "uncompressedSize".to_string(),
963                field_type: FieldType::Scalar(TypeCode::Int64),
964            },
965            FieldDesc {
966                name: "dimension".to_string(),
967                field_type: FieldType::StructureArray(StructureDesc {
968                    struct_id: Some("dimension_t".to_string()),
969                    fields: vec![
970                        FieldDesc {
971                            name: "size".to_string(),
972                            field_type: FieldType::Scalar(TypeCode::Int32),
973                        },
974                        FieldDesc {
975                            name: "offset".to_string(),
976                            field_type: FieldType::Scalar(TypeCode::Int32),
977                        },
978                        FieldDesc {
979                            name: "fullSize".to_string(),
980                            field_type: FieldType::Scalar(TypeCode::Int32),
981                        },
982                        FieldDesc {
983                            name: "binning".to_string(),
984                            field_type: FieldType::Scalar(TypeCode::Int32),
985                        },
986                        FieldDesc {
987                            name: "reverse".to_string(),
988                            field_type: FieldType::Scalar(TypeCode::Boolean),
989                        },
990                    ],
991                }),
992            },
993            FieldDesc {
994                name: "uniqueId".to_string(),
995                field_type: FieldType::Scalar(TypeCode::Int32),
996            },
997            FieldDesc {
998                name: "dataTimeStamp".to_string(),
999                field_type: FieldType::Structure(timestamp_desc()),
1000            },
1001            FieldDesc {
1002                name: "attribute".to_string(),
1003                field_type: FieldType::StructureArray(StructureDesc {
1004                    struct_id: Some("NTAttribute".to_string()),
1005                    fields: vec![
1006                        FieldDesc {
1007                            name: "name".to_string(),
1008                            field_type: FieldType::String,
1009                        },
1010                        FieldDesc {
1011                            name: "value".to_string(),
1012                            field_type: FieldType::Variant,
1013                        },
1014                        FieldDesc {
1015                            name: "descriptor".to_string(),
1016                            field_type: FieldType::String,
1017                        },
1018                        FieldDesc {
1019                            name: "sourceType".to_string(),
1020                            field_type: FieldType::Scalar(TypeCode::Int32),
1021                        },
1022                        FieldDesc {
1023                            name: "source".to_string(),
1024                            field_type: FieldType::String,
1025                        },
1026                    ],
1027                }),
1028            },
1029            FieldDesc {
1030                name: "descriptor".to_string(),
1031                field_type: FieldType::String,
1032            },
1033            FieldDesc {
1034                name: "alarm".to_string(),
1035                field_type: FieldType::Structure(alarm_desc()),
1036            },
1037            FieldDesc {
1038                name: "timeStamp".to_string(),
1039                field_type: FieldType::Structure(timestamp_desc()),
1040            },
1041            FieldDesc {
1042                name: "display".to_string(),
1043                field_type: FieldType::Structure(display_desc()),
1044            },
1045        ],
1046    }
1047}
1048
1049fn encode_attribute_variant(attr: &NtAttribute, is_be: bool) -> Vec<u8> {
1050    match &attr.value {
1051        ScalarValue::Bool(v) => {
1052            let mut out = vec![TypeCode::Boolean as u8];
1053            out.push(if *v { 1 } else { 0 });
1054            out
1055        }
1056        ScalarValue::I8(v) => {
1057            let mut out = vec![TypeCode::Int8 as u8];
1058            out.push(*v as u8);
1059            out
1060        }
1061        ScalarValue::I16(v) => {
1062            let mut out = vec![TypeCode::Int16 as u8];
1063            out.extend_from_slice(&if is_be {
1064                v.to_be_bytes().to_vec()
1065            } else {
1066                v.to_le_bytes().to_vec()
1067            });
1068            out
1069        }
1070        ScalarValue::I32(v) => {
1071            let mut out = vec![TypeCode::Int32 as u8];
1072            out.extend_from_slice(&encode_i32(*v, is_be));
1073            out
1074        }
1075        ScalarValue::I64(v) => {
1076            let mut out = vec![TypeCode::Int64 as u8];
1077            out.extend_from_slice(&encode_i64(*v, is_be));
1078            out
1079        }
1080        ScalarValue::U8(v) => {
1081            let mut out = vec![TypeCode::UInt8 as u8];
1082            out.push(*v);
1083            out
1084        }
1085        ScalarValue::U16(v) => {
1086            let mut out = vec![TypeCode::UInt16 as u8];
1087            out.extend_from_slice(&if is_be {
1088                v.to_be_bytes().to_vec()
1089            } else {
1090                v.to_le_bytes().to_vec()
1091            });
1092            out
1093        }
1094        ScalarValue::U32(v) => {
1095            let mut out = vec![TypeCode::UInt32 as u8];
1096            out.extend_from_slice(&if is_be {
1097                v.to_be_bytes().to_vec()
1098            } else {
1099                v.to_le_bytes().to_vec()
1100            });
1101            out
1102        }
1103        ScalarValue::U64(v) => {
1104            let mut out = vec![TypeCode::UInt64 as u8];
1105            out.extend_from_slice(&if is_be {
1106                v.to_be_bytes().to_vec()
1107            } else {
1108                v.to_le_bytes().to_vec()
1109            });
1110            out
1111        }
1112        ScalarValue::F32(v) => {
1113            let mut out = vec![TypeCode::Float32 as u8];
1114            out.extend_from_slice(&if is_be {
1115                v.to_be_bytes().to_vec()
1116            } else {
1117                v.to_le_bytes().to_vec()
1118            });
1119            out
1120        }
1121        ScalarValue::F64(v) => {
1122            let mut out = vec![TypeCode::Float64 as u8];
1123            out.extend_from_slice(&encode_f64(*v, is_be));
1124            out
1125        }
1126        ScalarValue::Str(v) => {
1127            let mut out = vec![TypeCode::String as u8];
1128            out.extend_from_slice(&encode_string_pvd(v, is_be));
1129            out
1130        }
1131    }
1132}
1133
1134pub fn encode_nt_ndarray_full(nt: &NtNdArray, is_be: bool) -> Vec<u8> {
1135    let mut out = Vec::new();
1136    out.extend_from_slice(&encode_ndarray_union(&nt.value, is_be));
1137    out.extend_from_slice(&encode_string_pvd(&nt.codec.name, is_be));
1138    out.extend_from_slice(&encode_codec_parameters(&nt.codec.parameters, is_be));
1139    out.extend_from_slice(&encode_i64(nt.compressed_size, is_be));
1140    out.extend_from_slice(&encode_i64(nt.uncompressed_size, is_be));
1141    out.extend_from_slice(&encode_size_pvd(nt.dimension.len(), is_be));
1142    for NdDimension {
1143        size,
1144        offset,
1145        full_size,
1146        binning,
1147        reverse,
1148    } in &nt.dimension
1149    {
1150        out.push(1); // non-null element indicator
1151        out.extend_from_slice(&encode_i32(*size, is_be));
1152        out.extend_from_slice(&encode_i32(*offset, is_be));
1153        out.extend_from_slice(&encode_i32(*full_size, is_be));
1154        out.extend_from_slice(&encode_i32(*binning, is_be));
1155        out.push(if *reverse { 1 } else { 0 });
1156    }
1157    out.extend_from_slice(&encode_i32(nt.unique_id, is_be));
1158    out.extend_from_slice(&encode_nt_timestamp(&nt.data_time_stamp, is_be));
1159    out.extend_from_slice(&encode_size_pvd(nt.attribute.len(), is_be));
1160    for attr in &nt.attribute {
1161        out.push(1); // non-null element indicator
1162        out.extend_from_slice(&encode_string_pvd(&attr.name, is_be));
1163        out.extend_from_slice(&encode_attribute_variant(attr, is_be));
1164        out.extend_from_slice(&encode_string_pvd(&attr.descriptor, is_be));
1165        out.extend_from_slice(&encode_i32(attr.source_type, is_be));
1166        out.extend_from_slice(&encode_string_pvd(&attr.source, is_be));
1167    }
1168    out.extend_from_slice(&encode_string_pvd(
1169        nt.descriptor.as_deref().unwrap_or(""),
1170        is_be,
1171    ));
1172    out.extend_from_slice(&encode_nt_alarm(
1173        nt.alarm.as_ref().unwrap_or(&NtAlarm::default()),
1174        is_be,
1175    ));
1176    out.extend_from_slice(&encode_nt_timestamp(
1177        nt.time_stamp.as_ref().unwrap_or(&NtTimeStamp::default()),
1178        is_be,
1179    ));
1180    out.extend_from_slice(&encode_nt_display(
1181        nt.display.as_ref().unwrap_or(&NtDisplay::default()),
1182        is_be,
1183    ));
1184    out
1185}
1186
1187// ---------------------------------------------------------------------------
1188// NTEnum descriptor & encoder
1189// ---------------------------------------------------------------------------
1190
1191pub fn nt_enum_desc() -> StructureDesc {
1192    StructureDesc {
1193        struct_id: Some("epics:nt/NTEnum:1.0".to_string()),
1194        fields: vec![
1195            FieldDesc {
1196                name: "value".to_string(),
1197                field_type: FieldType::Structure(StructureDesc {
1198                    struct_id: Some("enum_t".to_string()),
1199                    fields: vec![
1200                        FieldDesc {
1201                            name: "index".to_string(),
1202                            field_type: FieldType::Scalar(TypeCode::Int32),
1203                        },
1204                        FieldDesc {
1205                            name: "choices".to_string(),
1206                            field_type: FieldType::StringArray,
1207                        },
1208                    ],
1209                }),
1210            },
1211            FieldDesc {
1212                name: "alarm".to_string(),
1213                field_type: FieldType::Structure(alarm_desc()),
1214            },
1215            FieldDesc {
1216                name: "timeStamp".to_string(),
1217                field_type: FieldType::Structure(timestamp_desc()),
1218            },
1219        ],
1220    }
1221}
1222
1223pub fn encode_nt_enum_full(nt: &NtEnum, is_be: bool) -> Vec<u8> {
1224    let mut out = Vec::new();
1225    // value — enum_t { index, choices }
1226    out.extend_from_slice(&encode_enum(nt.index, &nt.choices, is_be));
1227    // alarm
1228    out.extend_from_slice(&encode_nt_alarm(&nt.alarm, is_be));
1229    // timeStamp
1230    out.extend_from_slice(&encode_nt_timestamp(&nt.time_stamp, is_be));
1231    out
1232}
1233
1234// ---------------------------------------------------------------------------
1235// PvValue (generic recursive) descriptor & encoder
1236// ---------------------------------------------------------------------------
1237
1238fn scalar_value_type_code(v: &ScalarValue) -> TypeCode {
1239    match v {
1240        ScalarValue::Bool(_) => TypeCode::Boolean,
1241        ScalarValue::I8(_) => TypeCode::Int8,
1242        ScalarValue::I16(_) => TypeCode::Int16,
1243        ScalarValue::I32(_) => TypeCode::Int32,
1244        ScalarValue::I64(_) => TypeCode::Int64,
1245        ScalarValue::U8(_) => TypeCode::UInt8,
1246        ScalarValue::U16(_) => TypeCode::UInt16,
1247        ScalarValue::U32(_) => TypeCode::UInt32,
1248        ScalarValue::U64(_) => TypeCode::UInt64,
1249        ScalarValue::F32(_) => TypeCode::Float32,
1250        ScalarValue::F64(_) => TypeCode::Float64,
1251        ScalarValue::Str(_) => TypeCode::String,
1252    }
1253}
1254
1255/// Build a [`StructureDesc`] from a [`PvValue::Structure`].
1256pub fn pv_value_desc(struct_id: &str, fields: &[(String, PvValue)]) -> StructureDesc {
1257    StructureDesc {
1258        struct_id: if struct_id.is_empty() {
1259            None
1260        } else {
1261            Some(struct_id.to_string())
1262        },
1263        fields: fields
1264            .iter()
1265            .map(|(name, val)| FieldDesc {
1266                name: name.clone(),
1267                field_type: pv_value_field_type(val),
1268            })
1269            .collect(),
1270    }
1271}
1272
1273fn pv_value_field_type(val: &PvValue) -> FieldType {
1274    match val {
1275        PvValue::Scalar(sv) => {
1276            if matches!(sv, ScalarValue::Str(_)) {
1277                FieldType::String
1278            } else {
1279                FieldType::Scalar(scalar_value_type_code(sv))
1280            }
1281        }
1282        PvValue::ScalarArray(sa) => scalar_array_field_type(sa),
1283        PvValue::Structure { struct_id, fields } => {
1284            FieldType::Structure(pv_value_desc(struct_id, fields))
1285        }
1286    }
1287}
1288
1289/// Encode a [`PvValue`] tree to PVA wire bytes (values only, no descriptor).
1290pub fn encode_pv_value(val: &PvValue, is_be: bool) -> Vec<u8> {
1291    match val {
1292        PvValue::Scalar(sv) => encode_scalar_value(sv, is_be),
1293        PvValue::ScalarArray(sa) => encode_scalar_array_value_pvd(sa, is_be),
1294        PvValue::Structure { fields, .. } => {
1295            let mut out = Vec::new();
1296            for (_, v) in fields {
1297                out.extend_from_slice(&encode_pv_value(v, is_be));
1298            }
1299            out
1300        }
1301    }
1302}
1303
1304pub fn nt_payload_desc(payload: &NtPayload) -> StructureDesc {
1305    match payload {
1306        NtPayload::Scalar(nt) => nt_scalar_desc(&nt.value),
1307        NtPayload::ScalarArray(nt) => nt_scalar_array_desc(&nt.value),
1308        NtPayload::Table(nt) => nt_table_desc(nt),
1309        NtPayload::NdArray(nt) => nt_ndarray_desc(nt),
1310        NtPayload::Enum(_) => nt_enum_desc(),
1311        NtPayload::Generic { struct_id, fields } => pv_value_desc(struct_id, fields),
1312    }
1313}
1314
1315pub fn encode_nt_payload_full(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1316    match payload {
1317        NtPayload::Scalar(nt) => encode_nt_scalar_full(nt, is_be),
1318        NtPayload::ScalarArray(nt) => encode_nt_scalar_array_full(nt, is_be),
1319        NtPayload::Table(nt) => encode_nt_table_full(nt, is_be),
1320        NtPayload::NdArray(nt) => encode_nt_ndarray_full(nt, is_be),
1321        NtPayload::Enum(nt) => encode_nt_enum_full(nt, is_be),
1322        NtPayload::Generic { fields, .. } => {
1323            let mut out = Vec::new();
1324            for (_, v) in fields {
1325                out.extend_from_slice(&encode_pv_value(v, is_be));
1326            }
1327            out
1328        }
1329    }
1330}
1331
1332pub fn encode_nt_payload_bitset(payload: &NtPayload, is_be: bool) -> Vec<u8> {
1333    let desc = nt_payload_desc(payload);
1334    let mut out = Vec::new();
1335    out.extend_from_slice(&encode_structure_bitset(&desc, is_be));
1336    out.extend_from_slice(&encode_nt_payload_full(payload, is_be));
1337    out
1338}
1339
1340pub fn encode_nt_payload_bitset_parts(payload: &NtPayload, is_be: bool) -> (Vec<u8>, Vec<u8>) {
1341    let desc = nt_payload_desc(payload);
1342    (
1343        encode_structure_bitset(&desc, is_be),
1344        encode_nt_payload_full(payload, is_be),
1345    )
1346}
1347
1348// ---------------------------------------------------------------------------
1349// Generic DecodedValue → wire bytes encoder
1350// ---------------------------------------------------------------------------
1351
1352use crate::spvd_decode::DecodedValue;
1353
1354/// Encode a `DecodedValue` back to PVA wire bytes.
1355pub fn encode_decoded_value(val: &DecodedValue, is_be: bool) -> Vec<u8> {
1356    match val {
1357        DecodedValue::Null => Vec::new(),
1358        DecodedValue::Boolean(v) => vec![if *v { 1 } else { 0 }],
1359        DecodedValue::Int8(v) => vec![*v as u8],
1360        DecodedValue::Int16(v) => {
1361            if is_be {
1362                v.to_be_bytes().to_vec()
1363            } else {
1364                v.to_le_bytes().to_vec()
1365            }
1366        }
1367        DecodedValue::Int32(v) => encode_i32(*v, is_be),
1368        DecodedValue::Int64(v) => encode_i64(*v, is_be),
1369        DecodedValue::UInt8(v) => vec![*v],
1370        DecodedValue::UInt16(v) => {
1371            if is_be {
1372                v.to_be_bytes().to_vec()
1373            } else {
1374                v.to_le_bytes().to_vec()
1375            }
1376        }
1377        DecodedValue::UInt32(v) => {
1378            if is_be {
1379                v.to_be_bytes().to_vec()
1380            } else {
1381                v.to_le_bytes().to_vec()
1382            }
1383        }
1384        DecodedValue::UInt64(v) => {
1385            if is_be {
1386                v.to_be_bytes().to_vec()
1387            } else {
1388                v.to_le_bytes().to_vec()
1389            }
1390        }
1391        DecodedValue::Float32(v) => {
1392            if is_be {
1393                v.to_be_bytes().to_vec()
1394            } else {
1395                v.to_le_bytes().to_vec()
1396            }
1397        }
1398        DecodedValue::Float64(v) => encode_f64(*v, is_be),
1399        DecodedValue::String(v) => encode_string_pvd(v, is_be),
1400        DecodedValue::Array(arr) => {
1401            let mut out = encode_size_pvd(arr.len(), is_be);
1402            for item in arr {
1403                out.extend_from_slice(&encode_decoded_value(item, is_be));
1404            }
1405            out
1406        }
1407        DecodedValue::Structure(fields) => {
1408            let mut out = Vec::new();
1409            for (_name, value) in fields {
1410                out.extend_from_slice(&encode_decoded_value(value, is_be));
1411            }
1412            out
1413        }
1414        DecodedValue::Raw(data) => data.clone(),
1415    }
1416}
1417
1418// ---------------------------------------------------------------------------
1419// pvRequest parsing & descriptor filtering
1420// ---------------------------------------------------------------------------
1421
1422/// Parse a pvRequest structure from the INIT body bytes and return the list
1423/// of requested field paths.
1424///
1425/// Paths are returned as dot-separated strings (e.g. `"value"`,
1426/// `"alarm.severity"`). An empty inner `field {}` structure, or a body that
1427/// cannot be parsed, is reported as `None`, meaning "return all fields" (no
1428/// filtering).
1429///
1430/// A field whose inner pvRequest sub-structure is itself empty selects the
1431/// whole sub-tree rooted at that field (so `field(alarm)` → `["alarm"]`
1432/// selects the entire `alarm` structure). Non-empty sub-structures expand
1433/// into one entry per leaf path (so `field(alarm{severity}) →
1434/// ["alarm.severity"]`).
1435pub fn decode_pv_request_fields(body: &[u8], is_be: bool) -> Option<Vec<String>> {
1436    if body.is_empty() {
1437        return None;
1438    }
1439    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1440    let desc = decoder.parse_introspection(body)?;
1441    for field in &desc.fields {
1442        if field.name == "field" {
1443            if let FieldType::Structure(ref inner) = field.field_type {
1444                if inner.fields.is_empty() {
1445                    return None;
1446                }
1447                let mut paths = Vec::new();
1448                collect_pv_request_paths(inner, "", &mut paths);
1449                if paths.is_empty() {
1450                    return None;
1451                }
1452                return Some(paths);
1453            }
1454        }
1455    }
1456    None
1457}
1458
1459fn collect_pv_request_paths(desc: &StructureDesc, prefix: &str, out: &mut Vec<String>) {
1460    for field in &desc.fields {
1461        let joined = if prefix.is_empty() {
1462            field.name.clone()
1463        } else {
1464            format!("{}.{}", prefix, field.name)
1465        };
1466        match &field.field_type {
1467            FieldType::Structure(nested) if !nested.fields.is_empty() => {
1468                collect_pv_request_paths(nested, &joined, out);
1469            }
1470            _ => out.push(joined),
1471        }
1472    }
1473}
1474
1475/// Decode the `record._options` key/value pairs from a pvRequest body.
1476///
1477/// Returns `None` if the pvRequest does not include a `record._options`
1478/// substructure, or if the option values cannot be decoded as strings.
1479pub fn decode_pv_request_options(body: &[u8], is_be: bool) -> Option<Vec<(String, String)>> {
1480    if body.is_empty() {
1481        return None;
1482    }
1483    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1484    let desc = decoder.parse_introspection(body)?;
1485    let options_desc = desc.fields.iter().find_map(|f| {
1486        if f.name != "record" {
1487            return None;
1488        }
1489        if let FieldType::Structure(inner) = &f.field_type {
1490            inner.fields.iter().find_map(|g| {
1491                if g.name != "_options" {
1492                    return None;
1493                }
1494                if let FieldType::Structure(opts) = &g.field_type {
1495                    Some(opts.clone())
1496                } else {
1497                    None
1498                }
1499            })
1500        } else {
1501            None
1502        }
1503    })?;
1504
1505    // The pvRequest body is `0x80 <desc> <values>`. The `field` sub-tree
1506    // encodes empty structs only, so contributes no value bytes; the
1507    // option strings follow immediately after the descriptor.
1508    let desc_bytes = encode_structure_desc(&desc, is_be);
1509    let values_start = 1 + desc_bytes.len();
1510    if values_start > body.len() {
1511        return None;
1512    }
1513    let mut cursor = &body[values_start..];
1514    let mut out = Vec::with_capacity(options_desc.fields.len());
1515    for f in &options_desc.fields {
1516        if !matches!(f.field_type, FieldType::String) {
1517            return None;
1518        }
1519        let (s, consumed) = crate::epics_decode::decode_string(cursor, is_be)?;
1520        out.push((f.name.clone(), s));
1521        cursor = &cursor[consumed..];
1522    }
1523    Some(out)
1524}
1525
1526/// Filter a [`StructureDesc`] to include only the listed field paths.
1527///
1528/// Paths may be dot-separated to descend into nested structures (e.g.
1529/// `"alarm.severity"`). A bare name selects the entire sub-tree rooted at
1530/// that field. Unknown paths are silently dropped. If `requested` is empty
1531/// the original descriptor is returned unchanged.
1532pub fn filter_structure_desc(desc: &StructureDesc, requested: &[String]) -> StructureDesc {
1533    if requested.is_empty() {
1534        return desc.clone();
1535    }
1536    let tree = build_path_tree(requested);
1537    prune_structure(desc, &tree)
1538}
1539
1540#[derive(Default, Debug, Clone)]
1541struct PathNode {
1542    /// When true, the whole sub-tree rooted at this node is selected and
1543    /// `children` should be ignored.
1544    select_all: bool,
1545    /// Insertion-ordered children. Using a Vec of pairs rather than a map
1546    /// preserves the field order implied by the caller, which matters for
1547    /// the PVA wire format (introspection field order is significant).
1548    children: Vec<(String, PathNode)>,
1549}
1550
1551impl PathNode {
1552    fn child_mut(&mut self, name: &str) -> &mut PathNode {
1553        if let Some(idx) = self.children.iter().position(|(n, _)| n == name) {
1554            return &mut self.children[idx].1;
1555        }
1556        self.children.push((name.to_string(), PathNode::default()));
1557        &mut self.children.last_mut().unwrap().1
1558    }
1559
1560    fn child(&self, name: &str) -> Option<&PathNode> {
1561        self.children
1562            .iter()
1563            .find(|(n, _)| n == name)
1564            .map(|(_, c)| c)
1565    }
1566}
1567
1568fn build_path_tree(paths: &[String]) -> PathNode {
1569    let mut root = PathNode::default();
1570    for p in paths {
1571        let parts: Vec<&str> = p.split('.').filter(|s| !s.is_empty()).collect();
1572        if parts.is_empty() {
1573            continue;
1574        }
1575        let mut node = &mut root;
1576        for (i, part) in parts.iter().enumerate() {
1577            let is_last = i == parts.len() - 1;
1578            let child = node.child_mut(part);
1579            if is_last {
1580                child.select_all = true;
1581                child.children.clear();
1582            }
1583            node = child;
1584        }
1585    }
1586    root
1587}
1588
1589fn prune_structure(desc: &StructureDesc, node: &PathNode) -> StructureDesc {
1590    if node.select_all {
1591        return desc.clone();
1592    }
1593    let mut fields = Vec::new();
1594    for field in &desc.fields {
1595        let Some(child) = node.child(&field.name) else {
1596            continue;
1597        };
1598        if child.select_all {
1599            fields.push(field.clone());
1600            continue;
1601        }
1602        match &field.field_type {
1603            FieldType::Structure(inner) => {
1604                let pruned = prune_structure(inner, child);
1605                if !pruned.fields.is_empty() {
1606                    fields.push(FieldDesc {
1607                        name: field.name.clone(),
1608                        field_type: FieldType::Structure(pruned),
1609                    });
1610                }
1611            }
1612            FieldType::StructureArray(inner) => {
1613                // For structure arrays we can only narrow the element
1614                // descriptor; we never drop the array field itself when a
1615                // sub-path is requested.
1616                let pruned = prune_structure(inner, child);
1617                if !pruned.fields.is_empty() {
1618                    fields.push(FieldDesc {
1619                        name: field.name.clone(),
1620                        field_type: FieldType::StructureArray(pruned),
1621                    });
1622                }
1623            }
1624            _ => {
1625                // Leaf referenced with a deeper path – drop it (unresolved).
1626            }
1627        }
1628    }
1629    StructureDesc {
1630        struct_id: desc.struct_id.clone(),
1631        fields,
1632    }
1633}
1634
1635/// Encode only the fields of an [`NtPayload`] whose paths appear in
1636/// `filtered_desc`.  The bitset and value bytes are computed against the
1637/// filtered descriptor so that a client that received the filtered INIT
1638/// descriptor will decode them correctly.
1639///
1640/// Supports nested filtering (e.g. a filtered descriptor that only contains
1641/// `alarm.severity`).
1642pub fn encode_nt_payload_filtered(
1643    payload: &NtPayload,
1644    filtered_desc: &StructureDesc,
1645    is_be: bool,
1646) -> (Vec<u8>, Vec<u8>) {
1647    let bitset = encode_structure_bitset(filtered_desc, is_be);
1648    let values = encode_nt_payload_values_for_desc(payload, filtered_desc, is_be);
1649    (bitset, values)
1650}
1651
1652/// Encode the value bytes of an `NtPayload` projected onto a (possibly
1653/// narrowed) descriptor. Fields not represented in `desc` are omitted;
1654/// sub-structures are encoded recursively.
1655pub fn encode_nt_payload_values_for_desc(
1656    payload: &NtPayload,
1657    desc: &StructureDesc,
1658    is_be: bool,
1659) -> Vec<u8> {
1660    let full_desc = nt_payload_desc(payload);
1661    if structure_desc_equal(&full_desc, desc) {
1662        // Fast path: no narrowing.
1663        return encode_nt_payload_full(payload, is_be);
1664    }
1665    let decoded = decode_payload_to_structure(payload, is_be)
1666        .unwrap_or_else(|| DecodedValue::Structure(Vec::new()));
1667    encode_decoded_projected(&decoded, desc, is_be)
1668}
1669
1670fn structure_desc_equal(a: &StructureDesc, b: &StructureDesc) -> bool {
1671    if a.struct_id != b.struct_id {
1672        return false;
1673    }
1674    if a.fields.len() != b.fields.len() {
1675        return false;
1676    }
1677    a.fields
1678        .iter()
1679        .zip(&b.fields)
1680        .all(|(x, y)| x.name == y.name && field_type_equal(&x.field_type, &y.field_type))
1681}
1682
1683fn field_type_equal(a: &FieldType, b: &FieldType) -> bool {
1684    match (a, b) {
1685        (FieldType::Scalar(x), FieldType::Scalar(y)) => x == y,
1686        (FieldType::ScalarArray(x), FieldType::ScalarArray(y)) => x == y,
1687        (FieldType::String, FieldType::String) => true,
1688        (FieldType::StringArray, FieldType::StringArray) => true,
1689        (FieldType::Structure(x), FieldType::Structure(y)) => structure_desc_equal(x, y),
1690        (FieldType::StructureArray(x), FieldType::StructureArray(y)) => structure_desc_equal(x, y),
1691        (FieldType::Variant, FieldType::Variant) => true,
1692        (FieldType::VariantArray, FieldType::VariantArray) => true,
1693        (FieldType::BoundedString(x), FieldType::BoundedString(y)) => x == y,
1694        // Treat unions as equal only by count (rare in NT; fine for fast-path).
1695        (FieldType::Union(x), FieldType::Union(y)) => x.len() == y.len(),
1696        (FieldType::UnionArray(x), FieldType::UnionArray(y)) => x.len() == y.len(),
1697        _ => false,
1698    }
1699}
1700
1701/// Round-trip an NtPayload through its full descriptor to obtain a
1702/// `DecodedValue::Structure` we can project against a narrowed descriptor.
1703fn decode_payload_to_structure(payload: &NtPayload, is_be: bool) -> Option<DecodedValue> {
1704    let desc = nt_payload_desc(payload);
1705    let bytes = encode_nt_payload_full(payload, is_be);
1706    let decoder = crate::spvd_decode::PvdDecoder::new(is_be);
1707    decoder.decode_structure(&bytes, &desc).map(|(v, _)| v)
1708}
1709
1710/// Re-encode a `DecodedValue::Structure` against a (possibly narrowed)
1711/// descriptor, omitting fields that are not present in the descriptor.
1712pub fn encode_decoded_projected(
1713    value: &DecodedValue,
1714    desc: &StructureDesc,
1715    is_be: bool,
1716) -> Vec<u8> {
1717    let DecodedValue::Structure(fields) = value else {
1718        // Fallback: not a structure – emit raw bytes.
1719        return encode_decoded_value(value, is_be);
1720    };
1721    let mut out = Vec::new();
1722    for target in &desc.fields {
1723        let Some((_, sub_value)) = fields.iter().find(|(n, _)| n == &target.name) else {
1724            continue;
1725        };
1726        match &target.field_type {
1727            FieldType::Structure(inner) => {
1728                out.extend_from_slice(&encode_decoded_projected(sub_value, inner, is_be));
1729            }
1730            _ => {
1731                out.extend_from_slice(&encode_decoded_value(sub_value, is_be));
1732            }
1733        }
1734    }
1735    out
1736}
1737
1738// ---------------------------------------------------------------------------
1739// Sparse delta encoding (Phase 3)
1740// ---------------------------------------------------------------------------
1741
1742/// Project an [`NtPayload`] onto `desc`, returning a [`DecodedValue::Structure`]
1743/// that contains only the fields represented in `desc`. Missing descriptor
1744/// fields are silently dropped.
1745fn project_payload_on_desc(payload: &NtPayload, desc: &StructureDesc, is_be: bool) -> DecodedValue {
1746    let decoded = decode_payload_to_structure(payload, is_be)
1747        .unwrap_or_else(|| DecodedValue::Structure(Vec::new()));
1748    project_decoded(&decoded, desc)
1749}
1750
1751fn project_decoded(value: &DecodedValue, desc: &StructureDesc) -> DecodedValue {
1752    let DecodedValue::Structure(fields) = value else {
1753        return value.clone();
1754    };
1755    let mut out: Vec<(String, DecodedValue)> = Vec::new();
1756    for target in &desc.fields {
1757        let Some((_, v)) = fields.iter().find(|(n, _)| n == &target.name) else {
1758            continue;
1759        };
1760        match &target.field_type {
1761            FieldType::Structure(inner) => {
1762                out.push((target.name.clone(), project_decoded(v, inner)));
1763            }
1764            _ => {
1765                out.push((target.name.clone(), v.clone()));
1766            }
1767        }
1768    }
1769    DecodedValue::Structure(out)
1770}
1771
1772/// Structural equality for [`DecodedValue`] with NaN treated as equal to NaN
1773/// (avoids spurious monitor flaps when a float field is NaN on both sides).
1774pub fn decoded_values_equal(a: &DecodedValue, b: &DecodedValue) -> bool {
1775    use DecodedValue::*;
1776    match (a, b) {
1777        (Null, Null) => true,
1778        (Boolean(x), Boolean(y)) => x == y,
1779        (Int8(x), Int8(y)) => x == y,
1780        (Int16(x), Int16(y)) => x == y,
1781        (Int32(x), Int32(y)) => x == y,
1782        (Int64(x), Int64(y)) => x == y,
1783        (UInt8(x), UInt8(y)) => x == y,
1784        (UInt16(x), UInt16(y)) => x == y,
1785        (UInt32(x), UInt32(y)) => x == y,
1786        (UInt64(x), UInt64(y)) => x == y,
1787        (Float32(x), Float32(y)) => x == y || (x.is_nan() && y.is_nan()),
1788        (Float64(x), Float64(y)) => x == y || (x.is_nan() && y.is_nan()),
1789        (String(x), String(y)) => x == y,
1790        (Raw(x), Raw(y)) => x == y,
1791        (Array(x), Array(y)) => {
1792            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| decoded_values_equal(a, b))
1793        }
1794        (Structure(x), Structure(y)) => {
1795            x.len() == y.len()
1796                && x.iter()
1797                    .zip(y)
1798                    .all(|((ln, lv), (rn, rv))| ln == rn && decoded_values_equal(lv, rv))
1799        }
1800        _ => false,
1801    }
1802}
1803
1804/// Walks `desc` in pre-order (matching the PVA wire convention that bit 0
1805/// represents the whole root structure and subsequent bits correspond to
1806/// fields in pre-order) and returns a per-bit flag vector marking leaves
1807/// whose value differs between `prev` and `next`.
1808///
1809/// Returns `None` if no leaves changed. Structure-type fields always have
1810/// their bit cleared — changes propagate to the descendants so a filtered
1811/// monitor client sees only the true differences.
1812pub fn compute_changed_bits(
1813    prev: &DecodedValue,
1814    next: &DecodedValue,
1815    desc: &StructureDesc,
1816) -> Option<Vec<bool>> {
1817    let total = 1 + spvd_count_structure_fields(desc);
1818    let mut bits = vec![false; total];
1819    let mut idx = 1usize;
1820    let any = fill_changed_bits(prev, next, desc, &mut bits, &mut idx);
1821    if any { Some(bits) } else { None }
1822}
1823
1824fn get_field_by_name<'a>(val: &'a DecodedValue, name: &str) -> Option<&'a DecodedValue> {
1825    match val {
1826        DecodedValue::Structure(f) => f.iter().find(|(n, _)| n == name).map(|(_, v)| v),
1827        _ => None,
1828    }
1829}
1830
1831fn fill_changed_bits(
1832    prev: &DecodedValue,
1833    next: &DecodedValue,
1834    desc: &StructureDesc,
1835    bits: &mut [bool],
1836    idx: &mut usize,
1837) -> bool {
1838    let mut any = false;
1839    for field in &desc.fields {
1840        let this = *idx;
1841        *idx += 1;
1842        let p = get_field_by_name(prev, &field.name);
1843        let n = get_field_by_name(next, &field.name);
1844        match &field.field_type {
1845            FieldType::Structure(inner) => {
1846                let empty = DecodedValue::Structure(Vec::new());
1847                let pv = p.unwrap_or(&empty);
1848                let nv = n.unwrap_or(&empty);
1849                if fill_changed_bits(pv, nv, inner, bits, idx) {
1850                    any = true;
1851                }
1852            }
1853            _ => {
1854                let changed = match (p, n) {
1855                    (Some(a), Some(b)) => !decoded_values_equal(a, b),
1856                    (Some(_), None) | (None, Some(_)) => true,
1857                    (None, None) => false,
1858                };
1859                if changed {
1860                    bits[this] = true;
1861                    any = true;
1862                }
1863            }
1864        }
1865    }
1866    any
1867}
1868
1869fn encode_values_for_bits(
1870    value: &DecodedValue,
1871    desc: &StructureDesc,
1872    bits: &[bool],
1873    idx: &mut usize,
1874    is_be: bool,
1875    out: &mut Vec<u8>,
1876) {
1877    for field in &desc.fields {
1878        let this = *idx;
1879        *idx += 1;
1880        let sub = get_field_by_name(value, &field.name);
1881        match &field.field_type {
1882            FieldType::Structure(inner) => {
1883                let empty = DecodedValue::Structure(Vec::new());
1884                let v = sub.unwrap_or(&empty);
1885                encode_values_for_bits(v, inner, bits, idx, is_be, out);
1886            }
1887            _ => {
1888                if bits[this] {
1889                    if let Some(v) = sub {
1890                        out.extend_from_slice(&encode_decoded_value(v, is_be));
1891                    }
1892                }
1893            }
1894        }
1895    }
1896}
1897
1898fn encode_bitset_from_flags(bits: &[bool], is_be: bool) -> Vec<u8> {
1899    let bitset_size = (bits.len() + 7) / 8;
1900    let mut bitset = vec![0u8; bitset_size];
1901    for (i, b) in bits.iter().enumerate() {
1902        if *b {
1903            bitset[i / 8] |= 1 << (i % 8);
1904        }
1905    }
1906    let mut out = Vec::new();
1907    out.extend_from_slice(&encode_size_pvd(bitset_size, is_be));
1908    out.extend_from_slice(&bitset);
1909    out
1910}
1911
1912/// Encode a sparse monitor-data delta between `prev` and `next` projected onto
1913/// `filtered_desc`. Returns `None` if nothing changed in the filtered view
1914/// (caller should suppress the update). Otherwise returns `(bitset, values)`
1915/// with only the changed leaves marked and encoded.
1916pub fn encode_nt_payload_delta(
1917    prev: &NtPayload,
1918    next: &NtPayload,
1919    filtered_desc: &StructureDesc,
1920    is_be: bool,
1921) -> Option<(Vec<u8>, Vec<u8>)> {
1922    let prev_proj = project_payload_on_desc(prev, filtered_desc, is_be);
1923    let next_proj = project_payload_on_desc(next, filtered_desc, is_be);
1924    let bits = compute_changed_bits(&prev_proj, &next_proj, filtered_desc)?;
1925    let bitset = encode_bitset_from_flags(&bits, is_be);
1926    let mut values = Vec::new();
1927    let mut idx = 1usize;
1928    encode_values_for_bits(
1929        &next_proj,
1930        filtered_desc,
1931        &bits,
1932        &mut idx,
1933        is_be,
1934        &mut values,
1935    );
1936    Some((bitset, values))
1937}
1938
1939fn spvd_count_structure_fields(desc: &StructureDesc) -> usize {
1940    let mut count = 0;
1941    for field in &desc.fields {
1942        count += 1;
1943        if let FieldType::Structure(inner) = &field.field_type {
1944            count += spvd_count_structure_fields(inner);
1945        }
1946    }
1947    count
1948}
1949
1950// ---------------------------------------------------------------------------
1951// pvRequest builder
1952// ---------------------------------------------------------------------------
1953// ---------------------------------------------------------------------------
1954// pvRequest builder
1955// ---------------------------------------------------------------------------
1956
1957/// Build a pvRequest structure for the given field paths.
1958///
1959/// Each entry may be a simple top-level name (e.g. `"value"`) or a
1960/// dot-separated nested path (e.g. `"alarm.severity"`,
1961/// `"timeStamp.secondsPastEpoch"`).
1962///
1963/// A bare name selects the entire sub-tree rooted at that field. Nested
1964/// paths produce the corresponding nested sub-structure in the pvRequest so
1965/// that a PVA server can filter down to the requested leaves.
1966///
1967/// Examples:
1968/// - `encode_pv_request(&["value", "alarm", "timeStamp"], false)` →
1969///   `field(value,alarm,timeStamp)`
1970/// - `encode_pv_request(&["alarm.severity"], false)` →
1971///   `field(alarm{severity})`
1972///
1973/// The output is the *full* type-described pvRequest structure: a `0x80`
1974/// tag followed by the structure descriptor and empty-struct field values.
1975pub fn encode_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
1976    encode_pv_request_with_options(fields, &[], is_be)
1977}
1978
1979/// Build a pvRequest structure with extra `record._options` key/value pairs.
1980///
1981/// `options` is an ordered list of `(name, value)` pairs (both strings) that
1982/// are encoded as `structure record { structure _options { string name; ... } }`
1983/// alongside the usual `field(...)` selector. This is the standard PVAccess
1984/// mechanism for requesting transport options such as
1985/// `pipeline=true,queueSize=N` on a monitor.
1986///
1987/// Empty `options` is equivalent to [`encode_pv_request`].
1988pub fn encode_pv_request_with_options(
1989    fields: &[&str],
1990    options: &[(&str, &str)],
1991    is_be: bool,
1992) -> Vec<u8> {
1993    let tree = build_path_tree_from_strs(fields);
1994    let inner_fields = path_tree_to_field_descs(&tree);
1995
1996    let field_desc = StructureDesc {
1997        struct_id: None,
1998        fields: inner_fields,
1999    };
2000
2001    let mut top_fields = vec![FieldDesc {
2002        name: "field".to_string(),
2003        field_type: FieldType::Structure(field_desc),
2004    }];
2005
2006    if !options.is_empty() {
2007        let options_desc = StructureDesc {
2008            struct_id: None,
2009            fields: options
2010                .iter()
2011                .map(|(k, _)| FieldDesc {
2012                    name: (*k).to_string(),
2013                    field_type: FieldType::String,
2014                })
2015                .collect(),
2016        };
2017        let record_desc = StructureDesc {
2018            struct_id: None,
2019            fields: vec![FieldDesc {
2020                name: "_options".to_string(),
2021                field_type: FieldType::Structure(options_desc),
2022            }],
2023        };
2024        top_fields.push(FieldDesc {
2025            name: "record".to_string(),
2026            field_type: FieldType::Structure(record_desc),
2027        });
2028    }
2029
2030    let pv_request_desc = StructureDesc {
2031        struct_id: None,
2032        fields: top_fields,
2033    };
2034
2035    let mut out = Vec::new();
2036    out.push(0x80); // structure tag
2037    out.extend_from_slice(&encode_structure_desc(&pv_request_desc, is_be));
2038    // Values: the `field` sub-tree is entirely empty structs (no leaves), so
2039    // it contributes no bytes. If options are present, append the string
2040    // values for record._options in declared order.
2041    for (_, v) in options {
2042        out.extend_from_slice(&encode_string_pvd(v, is_be));
2043    }
2044    out
2045}
2046
2047fn build_path_tree_from_strs(paths: &[&str]) -> PathNode {
2048    let owned: Vec<String> = paths.iter().map(|s| (*s).to_string()).collect();
2049    build_path_tree(&owned)
2050}
2051
2052fn path_tree_to_field_descs(node: &PathNode) -> Vec<FieldDesc> {
2053    node.children
2054        .iter()
2055        .map(|(name, child)| {
2056            let nested_fields = if child.select_all {
2057                Vec::new()
2058            } else {
2059                path_tree_to_field_descs(child)
2060            };
2061            FieldDesc {
2062                name: name.clone(),
2063                field_type: FieldType::Structure(StructureDesc {
2064                    struct_id: None,
2065                    fields: nested_fields,
2066                }),
2067            }
2068        })
2069        .collect()
2070}
2071
2072#[cfg(test)]
2073mod tests {
2074    use super::*;
2075    use crate::spvd_decode::PvdDecoder;
2076
2077    #[test]
2078    fn nt_scalar_roundtrip() {
2079        let nt = NtScalar::from_value(ScalarValue::F64(12.5));
2080        let desc = nt_scalar_desc(&nt.value);
2081        let desc_bytes = encode_structure_desc(&desc, false);
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, false));
2086
2087        let decoder = PvdDecoder::new(false);
2088        let parsed_desc = decoder.parse_introspection(&pvd).expect("desc");
2089        let (_, consumed) = decoder
2090            .decode_structure(&pvd[1 + desc_bytes.len()..], &parsed_desc)
2091            .expect("value");
2092        assert!(consumed > 0);
2093    }
2094
2095    // --- timeStamp encoding: stored value honored, stable, and distinct -----
2096    //
2097    // Regression tests for the "monitor seconds freeze, only nanoseconds tick"
2098    // bug: `encode_timestamp` used to always stamp `SystemTime::now()`, so the
2099    // delta path (which re-encodes prev and next at send time) sampled now()
2100    // twice microseconds apart — leaving secondsPastEpoch identical (never
2101    // flagged changed) while nanoseconds spuriously differed. A stored
2102    // `time_stamp` must be encoded verbatim and be stable across encodes so
2103    // that prev/next deltas compare correctly.
2104
2105    fn decode_nt_full(nt: &NtScalar, is_be: bool) -> DecodedValue {
2106        let desc = nt_scalar_desc(&nt.value);
2107        let desc_bytes = encode_structure_desc(&desc, is_be);
2108        let mut pvd = Vec::new();
2109        pvd.push(0x80);
2110        pvd.extend_from_slice(&desc_bytes);
2111        pvd.extend_from_slice(&encode_nt_scalar_full(nt, is_be));
2112        let decoder = PvdDecoder::new(is_be);
2113        let parsed_desc = decoder.parse_introspection(&pvd).expect("desc");
2114        let (val, _) = decoder
2115            .decode_structure(&pvd[1 + desc_bytes.len()..], &parsed_desc)
2116            .expect("value");
2117        val
2118    }
2119
2120    fn timestamp_fields(v: &DecodedValue) -> (i64, i32) {
2121        let DecodedValue::Structure(fields) = v else {
2122            panic!("top-level not a structure");
2123        };
2124        let (_, ts) = fields
2125            .iter()
2126            .find(|(n, _)| n == "timeStamp")
2127            .expect("timeStamp field");
2128        let DecodedValue::Structure(ts_fields) = ts else {
2129            panic!("timeStamp not a structure");
2130        };
2131        let mut secs = None;
2132        let mut nanos = None;
2133        for (n, val) in ts_fields {
2134            match (n.as_str(), val) {
2135                ("secondsPastEpoch", DecodedValue::Int64(s)) => secs = Some(*s),
2136                ("nanoseconds", DecodedValue::Int32(ns)) => nanos = Some(*ns),
2137                _ => {}
2138            }
2139        }
2140        (secs.expect("secondsPastEpoch"), nanos.expect("nanoseconds"))
2141    }
2142
2143    #[test]
2144    fn encode_timestamp_honors_stored_value() {
2145        for is_be in [false, true] {
2146            let nt = NtScalar::from_value(ScalarValue::F64(1.0)).with_timestamp(1_234_567_890, 42);
2147            let (secs, nanos) = timestamp_fields(&decode_nt_full(&nt, is_be));
2148            assert_eq!(
2149                secs, 1_234_567_890,
2150                "stored seconds must be encoded verbatim"
2151            );
2152            assert_eq!(nanos, 42, "stored nanoseconds must be encoded verbatim");
2153        }
2154    }
2155
2156    #[test]
2157    fn stored_timestamp_is_stable_across_encodes() {
2158        // The property that fixes the delta: encoding the same NtScalar twice
2159        // yields byte-identical timeStamps (unlike the now() fallback).
2160        let nt = NtScalar::from_value(ScalarValue::F64(1.0)).with_timestamp(1000, 500);
2161        let a = encode_nt_scalar_full(&nt, false);
2162        let b = encode_nt_scalar_full(&nt, false);
2163        assert_eq!(a, b, "a stored timestamp must be stable across encodes");
2164    }
2165
2166    #[test]
2167    fn distinct_stored_timestamps_flag_seconds_changed() {
2168        // Two same-value snapshots one second apart must produce a delta whose
2169        // secondsPastEpoch differs — i.e. the seconds are reported as changed.
2170        let prev =
2171            NtPayload::Scalar(NtScalar::from_value(ScalarValue::F64(1.0)).with_timestamp(1000, 0));
2172        let next =
2173            NtPayload::Scalar(NtScalar::from_value(ScalarValue::F64(1.0)).with_timestamp(1001, 0));
2174        let desc = nt_scalar_desc(&ScalarValue::F64(1.0));
2175        let (_bitset, values) =
2176            encode_nt_payload_delta(&prev, &next, &desc, false).expect("delta present");
2177        // The changed values must carry the new seconds (1001), not be empty.
2178        assert!(!values.is_empty(), "delta must carry changed field values");
2179        // Full-encode sanity: prev and next decode to different seconds.
2180        let NtPayload::Scalar(prev_nt) = &prev else {
2181            unreachable!()
2182        };
2183        let NtPayload::Scalar(next_nt) = &next else {
2184            unreachable!()
2185        };
2186        assert_eq!(timestamp_fields(&decode_nt_full(prev_nt, false)).0, 1000);
2187        assert_eq!(timestamp_fields(&decode_nt_full(next_nt, false)).0, 1001);
2188    }
2189
2190    #[test]
2191    fn none_timestamp_falls_back_to_now() {
2192        // Backward compatibility: no stored timestamp -> stamp current time.
2193        let nt = NtScalar::from_value(ScalarValue::F64(1.0));
2194        let (secs, _) = timestamp_fields(&decode_nt_full(&nt, false));
2195        assert!(
2196            secs > 1_700_000_000,
2197            "now() fallback should yield a recent epoch, got {secs}"
2198        );
2199    }
2200
2201    #[test]
2202    fn nt_ndarray_roundtrip() {
2203        use spvirit_types::{
2204            NdCodec, NdDimension, NtAlarm, NtNdArray, NtTimeStamp, ScalarArrayValue,
2205        };
2206        use std::collections::HashMap;
2207
2208        let nt = NtNdArray {
2209            value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
2210            codec: NdCodec {
2211                name: String::new(),
2212                parameters: HashMap::new(),
2213            },
2214            compressed_size: 4,
2215            uncompressed_size: 4,
2216            dimension: vec![NdDimension {
2217                size: 2,
2218                offset: 0,
2219                full_size: 2,
2220                binning: 1,
2221                reverse: false,
2222            }],
2223            unique_id: 42,
2224            data_time_stamp: NtTimeStamp {
2225                seconds_past_epoch: 1000,
2226                nanoseconds: 500,
2227                user_tag: 0,
2228            },
2229            attribute: Vec::new(),
2230            descriptor: Some("test".to_string()),
2231            alarm: Some(NtAlarm::default()),
2232            time_stamp: Some(NtTimeStamp::default()),
2233            display: None,
2234        };
2235
2236        let desc = nt_ndarray_desc(&nt);
2237        let desc_bytes = encode_structure_desc(&desc, false);
2238        let data_bytes = encode_nt_ndarray_full(&nt, false);
2239
2240        // Build complete PVD: type_tag + desc + data
2241        let mut pvd = Vec::new();
2242        pvd.push(0x80);
2243        pvd.extend_from_slice(&desc_bytes);
2244        pvd.extend_from_slice(&data_bytes);
2245
2246        let decoder = PvdDecoder::new(false);
2247        let parsed_desc = decoder
2248            .parse_introspection(&pvd)
2249            .expect("desc parse failed");
2250        let data_start = 1 + desc_bytes.len();
2251        let (_decoded, consumed) = decoder
2252            .decode_structure(&pvd[data_start..], &parsed_desc)
2253            .expect("data decode failed");
2254        assert!(consumed > 0, "consumed should be > 0");
2255        assert_eq!(
2256            consumed,
2257            data_bytes.len(),
2258            "consumed should match data_bytes.len()"
2259        );
2260    }
2261
2262    #[test]
2263    fn nt_table_wire_format_carries_metadata() {
2264        use spvirit_types::{NtTable, NtTableColumn, ScalarArrayValue};
2265
2266        // NTTable per the normative type spec carries descriptor, alarm and
2267        // timeStamp. The EPICS Archiver Appliance in particular requires a
2268        // top-level timeStamp field to archive a structure at all.
2269        let nt = NtTable {
2270            labels: vec!["a".to_string()],
2271            columns: vec![NtTableColumn {
2272                name: "a".to_string(),
2273                values: ScalarArrayValue::F64(vec![1.0, 2.0]),
2274            }],
2275            descriptor: Some("desc".to_string()),
2276            alarm: None,
2277            time_stamp: Some(NtTimeStamp {
2278                seconds_past_epoch: 1_700_000_000,
2279                nanoseconds: 42,
2280                user_tag: 0,
2281            }),
2282        };
2283
2284        let desc = nt_table_desc(&nt);
2285        let names: Vec<&str> = desc.fields.iter().map(|f| f.name.as_str()).collect();
2286        assert!(names.contains(&"timeStamp"), "desc must expose timeStamp");
2287        assert!(names.contains(&"alarm"), "desc must expose alarm");
2288        assert!(names.contains(&"descriptor"), "desc must expose descriptor");
2289
2290        // Encoded data must decode fully and consistently against the desc.
2291        let data_bytes = encode_nt_table_full(&nt, false);
2292        let decoder = PvdDecoder::new(false);
2293        let (decoded, consumed) = decoder
2294            .decode_structure(&data_bytes, &desc)
2295            .expect("data decode failed");
2296        assert_eq!(consumed, data_bytes.len());
2297        let DecodedValue::Structure(fields) = decoded else {
2298            panic!("expected structure");
2299        };
2300        let (_, ts) = fields
2301            .iter()
2302            .find(|(n, _)| n == "timeStamp")
2303            .expect("timeStamp value present");
2304        let DecodedValue::Structure(ts_fields) = ts else {
2305            panic!("expected timeStamp structure");
2306        };
2307        let (_, secs) = ts_fields
2308            .iter()
2309            .find(|(n, _)| n == "secondsPastEpoch")
2310            .expect("secondsPastEpoch present");
2311        assert!(
2312            matches!(secs, DecodedValue::Int64(1_700_000_000)),
2313            "expected secondsPastEpoch=1700000000, got {secs:?}"
2314        );
2315    }
2316
2317    #[test]
2318    fn pv_request_flat_roundtrip() {
2319        for is_be in [false, true] {
2320            let body = encode_pv_request(&["value", "alarm", "timeStamp"], is_be);
2321            let fields = decode_pv_request_fields(&body, is_be).expect("fields");
2322            assert_eq!(fields, vec!["value", "alarm", "timeStamp"]);
2323        }
2324    }
2325
2326    #[test]
2327    fn pv_request_nested_roundtrip() {
2328        let body = encode_pv_request(&["alarm.severity", "timeStamp.secondsPastEpoch"], false);
2329        let fields = decode_pv_request_fields(&body, false).expect("fields");
2330        assert_eq!(
2331            fields,
2332            vec![
2333                "alarm.severity".to_string(),
2334                "timeStamp.secondsPastEpoch".to_string()
2335            ]
2336        );
2337    }
2338
2339    #[test]
2340    fn pv_request_whole_subtree_beats_leaf() {
2341        // Requesting both "alarm" and "alarm.severity" should collapse to the
2342        // whole-subtree selection.
2343        let body = encode_pv_request(&["alarm.severity", "alarm"], false);
2344        let fields = decode_pv_request_fields(&body, false).expect("fields");
2345        assert_eq!(fields, vec!["alarm".to_string()]);
2346    }
2347
2348    #[test]
2349    fn pv_request_with_pipeline_options_roundtrip() {
2350        for is_be in [false, true] {
2351            let body = encode_pv_request_with_options(
2352                &["value", "alarm"],
2353                &[("pipeline", "true"), ("queueSize", "4")],
2354                is_be,
2355            );
2356            // Field selectors still parse correctly.
2357            let fields = decode_pv_request_fields(&body, is_be).expect("fields");
2358            assert_eq!(fields, vec!["value".to_string(), "alarm".to_string()]);
2359            // Options round-trip.
2360            let opts = decode_pv_request_options(&body, is_be).expect("opts");
2361            assert_eq!(
2362                opts,
2363                vec![
2364                    ("pipeline".to_string(), "true".to_string()),
2365                    ("queueSize".to_string(), "4".to_string()),
2366                ]
2367            );
2368        }
2369    }
2370
2371    #[test]
2372    fn pv_request_without_options_has_no_record() {
2373        let body = encode_pv_request(&["value"], false);
2374        assert!(decode_pv_request_options(&body, false).is_none());
2375    }
2376
2377    #[test]
2378    fn pv_request_empty_body_none() {
2379        assert!(decode_pv_request_fields(&[], false).is_none());
2380    }
2381
2382    #[test]
2383    fn filter_structure_desc_nested() {
2384        let alarm = StructureDesc {
2385            struct_id: Some("alarm_t".to_string()),
2386            fields: vec![
2387                FieldDesc {
2388                    name: "severity".into(),
2389                    field_type: FieldType::Scalar(TypeCode::Int32),
2390                },
2391                FieldDesc {
2392                    name: "status".into(),
2393                    field_type: FieldType::Scalar(TypeCode::Int32),
2394                },
2395                FieldDesc {
2396                    name: "message".into(),
2397                    field_type: FieldType::String,
2398                },
2399            ],
2400        };
2401        let desc = StructureDesc {
2402            struct_id: Some("epics:nt/NTScalar:1.0".into()),
2403            fields: vec![
2404                FieldDesc {
2405                    name: "value".into(),
2406                    field_type: FieldType::Scalar(TypeCode::Float64),
2407                },
2408                FieldDesc {
2409                    name: "alarm".into(),
2410                    field_type: FieldType::Structure(alarm.clone()),
2411                },
2412            ],
2413        };
2414
2415        let pruned = filter_structure_desc(&desc, &["alarm.severity".to_string()]);
2416        assert_eq!(pruned.fields.len(), 1);
2417        assert_eq!(pruned.fields[0].name, "alarm");
2418        match &pruned.fields[0].field_type {
2419            FieldType::Structure(inner) => {
2420                assert_eq!(inner.fields.len(), 1);
2421                assert_eq!(inner.fields[0].name, "severity");
2422            }
2423            other => panic!("expected Structure, got {:?}", other),
2424        }
2425
2426        // Whole-subtree selection preserves the full alarm.
2427        let pruned_all = filter_structure_desc(&desc, &["alarm".to_string()]);
2428        match &pruned_all.fields[0].field_type {
2429            FieldType::Structure(inner) => assert_eq!(inner.fields.len(), 3),
2430            other => panic!("expected Structure, got {:?}", other),
2431        }
2432
2433        // Unknown paths are silently dropped.
2434        let pruned_unknown = filter_structure_desc(&desc, &["nope".into(), "alarm.missing".into()]);
2435        assert!(pruned_unknown.fields.is_empty());
2436    }
2437
2438    #[test]
2439    fn filtered_monitor_round_trip_nested() {
2440        use crate::spvd_decode::PvdDecoder;
2441        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2442
2443        let mut nt = NtScalar::from_value(ScalarValue::F64(42.0));
2444        nt.alarm_severity = 2;
2445        nt.alarm_status = 7;
2446        nt.alarm_message = "hi".into();
2447        let payload = NtPayload::Scalar(nt);
2448
2449        // Client sends field(alarm.severity).
2450        let full_desc = nt_payload_desc(&payload);
2451        let paths = vec!["alarm.severity".to_string()];
2452        let filtered = filter_structure_desc(&full_desc, &paths);
2453        let (bitset, values) = encode_nt_payload_filtered(&payload, &filtered, false);
2454
2455        // Round-trip the filtered body using the filtered descriptor.
2456        let decoder = PvdDecoder::new(false);
2457        let mut body = bitset.clone();
2458        body.extend_from_slice(&values);
2459        let (decoded, _) = decoder
2460            .decode_structure_with_bitset(&body, &filtered)
2461            .expect("decode filtered");
2462
2463        let DecodedValue::Structure(fields) = decoded else {
2464            panic!("expected structure");
2465        };
2466        assert_eq!(fields.len(), 1);
2467        assert_eq!(fields[0].0, "alarm");
2468        match &fields[0].1 {
2469            DecodedValue::Structure(inner) => {
2470                assert_eq!(inner.len(), 1);
2471                assert_eq!(inner[0].0, "severity");
2472                assert!(matches!(inner[0].1, DecodedValue::Int32(2)));
2473            }
2474            other => panic!("expected Structure, got {:?}", other),
2475        }
2476
2477        // Verify that the filtered payload is genuinely smaller than an
2478        // unfiltered one (proves we're not emitting status/message bytes).
2479        let full_body_len = encode_nt_payload_full(&payload, false).len();
2480        assert!(values.len() < full_body_len);
2481    }
2482
2483    #[test]
2484    fn delta_returns_none_when_nothing_changed() {
2485        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2486        let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
2487        a.alarm_severity = 1;
2488        let p1 = NtPayload::Scalar(a.clone());
2489        let p2 = NtPayload::Scalar(a);
2490        let desc = filter_structure_desc(&nt_payload_desc(&p1), &["alarm.severity".to_string()]);
2491        assert!(encode_nt_payload_delta(&p1, &p2, &desc, false).is_none());
2492    }
2493
2494    #[test]
2495    fn delta_marks_only_changed_leaf() {
2496        use crate::spvd_decode::PvdDecoder;
2497        use spvirit_types::{NtPayload, NtScalar, ScalarValue};
2498
2499        let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
2500        a.alarm_severity = 1;
2501        a.alarm_status = 0;
2502        a.alarm_message = "ok".into();
2503        let mut b = a.clone();
2504        b.alarm_severity = 2; // only this leaf changes
2505        let p1 = NtPayload::Scalar(a);
2506        let p2 = NtPayload::Scalar(b);
2507        let desc = filter_structure_desc(
2508            &nt_payload_desc(&p1),
2509            &[
2510                "alarm.severity".to_string(),
2511                "alarm.status".to_string(),
2512                "alarm.message".to_string(),
2513            ],
2514        );
2515
2516        let (bitset, values) = encode_nt_payload_delta(&p1, &p2, &desc, false)
2517            .expect("delta must produce a frame when a leaf changed");
2518
2519        // Only one leaf bit set (severity). Bit layout for filtered_desc:
2520        //   bit 0 = root, bit 1 = alarm struct, bit 2 = severity,
2521        //   bit 3 = status, bit 4 = message.
2522        // => 5 bits → 1-byte bitset payload. The first byte is the size
2523        // prefix (1), followed by the bitset byte.
2524        assert_eq!(bitset[0], 1u8, "size prefix");
2525        let b0 = bitset[1];
2526        assert_eq!(b0 & 0x01, 0, "root bit must be clear");
2527        assert_eq!(b0 & 0x02, 0, "alarm struct bit must be clear");
2528        assert_eq!(b0 & 0x04, 0x04, "severity bit must be set");
2529        assert_eq!(b0 & 0x08, 0, "status bit must be clear");
2530        assert_eq!(b0 & 0x10, 0, "message bit must be clear");
2531
2532        // values should contain exactly one i32 (4 bytes).
2533        assert_eq!(values.len(), 4);
2534
2535        // Round-trip through the decoder and check severity.
2536        let decoder = PvdDecoder::new(false);
2537        let mut body = bitset.clone();
2538        body.extend_from_slice(&values);
2539        let (decoded, _) = decoder
2540            .decode_structure_with_bitset(&body, &desc)
2541            .expect("decode delta");
2542        let DecodedValue::Structure(fields) = decoded else {
2543            panic!("expected struct")
2544        };
2545        assert_eq!(fields.len(), 1);
2546        assert_eq!(fields[0].0, "alarm");
2547        match &fields[0].1 {
2548            DecodedValue::Structure(inner) => {
2549                assert_eq!(inner.len(), 1);
2550                assert_eq!(inner[0].0, "severity");
2551                assert!(matches!(inner[0].1, DecodedValue::Int32(2)));
2552            }
2553            other => panic!("expected struct got {:?}", other),
2554        }
2555    }
2556
2557    #[test]
2558    fn decoded_values_equal_treats_nan_as_equal() {
2559        let a = DecodedValue::Float64(f64::NAN);
2560        let b = DecodedValue::Float64(f64::NAN);
2561        assert!(decoded_values_equal(&a, &b));
2562        let c = DecodedValue::Float32(f32::NAN);
2563        let d = DecodedValue::Float32(f32::NAN);
2564        assert!(decoded_values_equal(&c, &d));
2565        // But different concrete values are still different.
2566        assert!(!decoded_values_equal(
2567            &DecodedValue::Float64(1.0),
2568            &DecodedValue::Float64(2.0)
2569        ));
2570    }
2571}