Skip to main content

m_bus_parser/
mbus_data.rs

1#[cfg(all(feature = "std", test))]
2use prettytable::{format, row, Table};
3use wireless_mbus_link_layer::WirelessFrame;
4
5use crate::user_data;
6use crate::MbusError;
7use wired_mbus_link_layer as frames;
8use wireless_mbus_link_layer;
9
10#[cfg_attr(
11    feature = "serde",
12    derive(serde::Serialize),
13    serde(bound(deserialize = "'de: 'a"))
14)]
15#[derive(Debug)]
16pub struct MbusData<'a, F> {
17    pub frame: F,
18    pub user_data: Option<user_data::UserDataBlock<'a>>,
19    pub data_records: Option<user_data::DataRecords<'a>>,
20    /// Application-layer parsing is deliberately non-fatal: callers can still
21    /// inspect a valid link-layer frame and report a partial decode.
22    #[cfg_attr(feature = "serde", serde(skip_serializing))]
23    pub application_error: Option<m_bus_core::ApplicationLayerError>,
24}
25
26impl<'a> TryFrom<&'a [u8]> for MbusData<'a, frames::WiredFrame<'a>> {
27    type Error = MbusError;
28
29    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
30        let frame = frames::WiredFrame::try_from(data)?;
31        let mut user_data = None;
32        let mut data_records = None;
33        let mut application_error = None;
34        match &frame {
35            frames::WiredFrame::LongFrame { data, .. } => {
36                match user_data::UserDataBlock::try_from(*data) {
37                    Ok(x) => {
38                        match &x {
39                            user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
40                                variable_data_block,
41                                ..
42                            }
43                            | user_data::UserDataBlock::VariableDataStructureWithShortTplHeader {
44                                variable_data_block,
45                                ..
46                            }
47                            | user_data::UserDataBlock::VariableDataStructureWithoutTplHeader {
48                                variable_data_block,
49                                ..
50                            } => {
51                                data_records = Some((*variable_data_block).into());
52                            }
53                            _ => {}
54                        }
55                        user_data = Some(x);
56                    }
57                    Err(error) => application_error = Some(error),
58                }
59            }
60            frames::WiredFrame::SingleCharacter { .. } => (),
61            frames::WiredFrame::ShortFrame { .. } => (),
62            frames::WiredFrame::ControlFrame { .. } => (),
63            _ => (),
64        };
65
66        Ok(MbusData {
67            frame,
68            user_data,
69            data_records,
70            application_error,
71        })
72    }
73}
74
75impl<'a> TryFrom<&'a [u8]> for MbusData<'a, WirelessFrame<'a>> {
76    type Error = MbusError;
77
78    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
79        let frame = wireless_mbus_link_layer::WirelessFrame::try_from(data)?;
80        let mut user_data = None;
81        let mut data_records = None;
82        let mut application_error = None;
83        // Extract application layer data from wireless frame
84        let wireless_mbus_link_layer::WirelessFrame { data, .. } = &frame;
85
86        match user_data::UserDataBlock::try_from(*data) {
87            Ok(user_data_block) => {
88                match &user_data_block {
89                    user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
90                        variable_data_block,
91                        ..
92                    } => {
93                        data_records = Some((*variable_data_block).into());
94                    }
95                    user_data::UserDataBlock::VariableDataStructureWithShortTplHeader {
96                        variable_data_block,
97                        ..
98                    } => {
99                        data_records = Some((*variable_data_block).into());
100                    }
101                    user_data::UserDataBlock::VariableDataStructureWithoutTplHeader {
102                        variable_data_block,
103                        ..
104                    } => {
105                        data_records = Some((*variable_data_block).into());
106                    }
107                    _ => {}
108                }
109                user_data = Some(user_data_block);
110            }
111            Err(error) => application_error = Some(error),
112        }
113
114        Ok(MbusData {
115            frame,
116            user_data,
117            data_records,
118            application_error,
119        })
120    }
121}
122
123#[cfg(feature = "std")]
124fn clean_and_convert(input: &str) -> Vec<u8> {
125    use core::str;
126    let input = input.trim();
127    let cleaned_data: String = input.replace("0x", "").replace([' ', ',', 'x'], "");
128
129    cleaned_data
130        .as_bytes()
131        .chunks(2)
132        .map(|chunk| {
133            let byte_str = str::from_utf8(chunk).unwrap_or_default();
134            u8::from_str_radix(byte_str, 16).unwrap_or_default()
135        })
136        .collect()
137}
138
139#[cfg(feature = "std")]
140#[must_use]
141pub fn serialize_mbus_data(data: &str, format: &str, key: Option<&[u8; 16]>) -> String {
142    match format {
143        "json-legacy" => parse_to_json(data, key),
144        "yaml-legacy" => parse_to_yaml(data, key),
145        "csv-wide" => parse_to_csv(data, key),
146        "annotated-legacy" => parse_to_annotated(data),
147        "hexview-legacy" => parse_to_hexview(data, key),
148        _ => match format.parse::<crate::output::OutputFormat>() {
149            Ok(output_format) => crate::output::render_hex(
150                data,
151                output_format,
152                &crate::output::RenderOptions {
153                    decode: crate::output::DecodeOptions {
154                        key: key.copied(),
155                        include_enrichment: true,
156                    },
157                    table_width: Some(100),
158                },
159            )
160            .unwrap_or_else(|error| format!("Error [{}]: {}", error.code(), error)),
161            Err(error) => format!("Error [{}]: {}", error.code(), error),
162        },
163    }
164}
165
166/// Normalized, human-readable interpretation of a parsed frame.
167///
168/// This is the single source of truth for the table and CSV renderers and is
169/// embedded as `summary` in the JSON and YAML outputs, so every output format
170/// exposes the same header information regardless of whether it comes from a
171/// long TPL header, a short TPL header or the wireless link layer.
172#[cfg(feature = "std")]
173#[derive(serde::Serialize)]
174struct FrameSummary {
175    frame_type: &'static str,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    function: Option<String>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    address: Option<String>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    identification_number: Option<String>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    manufacturer: Option<ManufacturerSummary>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    access_number: Option<u8>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    status: Option<String>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    security_mode: Option<String>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    version: Option<u8>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    device_type: Option<String>,
194    encrypted: bool,
195    decrypted: bool,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    encrypted_payload_hex: Option<String>,
198    records: Vec<RecordSummary>,
199}
200
201#[cfg(feature = "std")]
202#[derive(serde::Serialize)]
203struct ManufacturerSummary {
204    code: String,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    name: Option<String>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    website: Option<String>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    description: Option<String>,
211}
212
213#[cfg(feature = "std")]
214#[derive(serde::Serialize)]
215struct RecordSummary {
216    /// Human-readable rendering (same string the table and CSV outputs use).
217    display: String,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    value: Option<f64>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    exponent: Option<isize>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    unit: Option<String>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    quantity: Option<String>,
226    data_information: String,
227    header_hex: String,
228    data_hex: String,
229}
230
231#[cfg(feature = "std")]
232#[derive(serde::Serialize)]
233struct SummarySection<'a> {
234    summary: &'a FrameSummary,
235}
236
237#[cfg(feature = "std")]
238struct ParsedOutput {
239    summary: FrameSummary,
240    json: serde_json::Value,
241    yaml: String,
242}
243
244#[cfg(feature = "std")]
245impl FrameSummary {
246    fn new(frame_type: &'static str) -> Self {
247        Self {
248            frame_type,
249            function: None,
250            address: None,
251            identification_number: None,
252            manufacturer: None,
253            access_number: None,
254            status: None,
255            security_mode: None,
256            version: None,
257            device_type: None,
258            encrypted: false,
259            decrypted: false,
260            encrypted_payload_hex: None,
261            records: Vec::new(),
262        }
263    }
264}
265
266#[cfg(feature = "std")]
267fn manufacturer_summary(code: String) -> ManufacturerSummary {
268    let info = crate::manufacturers::lookup_manufacturer(&code);
269    ManufacturerSummary {
270        code,
271        name: info.as_ref().map(|i| i.name.to_string()),
272        website: info.as_ref().map(|i| i.website.to_string()),
273        description: info.as_ref().map(|i| i.description.to_string()),
274    }
275}
276
277#[cfg(feature = "std")]
278fn hex_string(data: &[u8]) -> String {
279    data.iter().map(|b| format!("{:02X}", b)).collect()
280}
281
282#[cfg(feature = "std")]
283fn user_data_is_encrypted(user_data: Option<&user_data::UserDataBlock>) -> bool {
284    use user_data::UserDataBlock;
285    match user_data {
286        Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
287            long_tpl_header, ..
288        }) => long_tpl_header.is_encrypted(),
289        Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
290            short_tpl_header, ..
291        }) => short_tpl_header.is_encrypted(),
292        _ => false,
293    }
294}
295
296#[cfg(feature = "std")]
297fn variable_data_block<'a>(user_data: Option<&user_data::UserDataBlock<'a>>) -> Option<&'a [u8]> {
298    use user_data::UserDataBlock;
299    match user_data {
300        Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
301            variable_data_block,
302            ..
303        })
304        | Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
305            variable_data_block,
306            ..
307        })
308        | Some(UserDataBlock::VariableDataStructureWithoutTplHeader {
309            variable_data_block,
310            ..
311        }) => Some(variable_data_block),
312        _ => None,
313    }
314}
315
316#[cfg(feature = "std")]
317fn record_summaries(records: &user_data::DataRecords) -> Vec<RecordSummary> {
318    use user_data::data_information::DataType;
319
320    records
321        .clone()
322        .flatten()
323        .map(|record| {
324            let value_information = record
325                .data_record_header
326                .processed_data_record_header
327                .value_information
328                .as_ref();
329            let value_information_display = match value_information {
330                Some(x) => format!("{}", x),
331                None => ")".to_string(),
332            };
333            let data_information = match record
334                .data_record_header
335                .processed_data_record_header
336                .data_information
337            {
338                Some(ref x) => format!("{}", x),
339                None => "None".to_string(),
340            };
341            let value = match record.data.value {
342                Some(DataType::Number(n)) | Some(DataType::LossyNumber(n)) => Some(n),
343                _ => None,
344            };
345            let unit = value_information
346                .map(|vi| vi.units().map(|unit| unit.to_string()).collect::<String>())
347                .filter(|s| !s.is_empty());
348            let quantity = value_information
349                .map(|vi| {
350                    vi.labels()
351                        .map(|label| format!("{:?}", label))
352                        .collect::<Vec<_>>()
353                        .join(", ")
354                })
355                .filter(|s| !s.is_empty());
356            RecordSummary {
357                display: format!("({}{}", record.data, value_information_display),
358                value,
359                exponent: value_information.map(|vi| vi.decimal_scale_exponent),
360                unit,
361                quantity,
362                data_information,
363                header_hex: record.data_record_header_hex(),
364                data_hex: record.data_hex(),
365            }
366        })
367        .collect()
368}
369
370#[cfg(feature = "std")]
371fn apply_user_data_summary(
372    summary: &mut FrameSummary,
373    user_data: Option<&user_data::UserDataBlock>,
374) {
375    use user_data::UserDataBlock;
376    match user_data {
377        Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
378            long_tpl_header, ..
379        }) => {
380            summary.identification_number = Some(long_tpl_header.identification_number.to_string());
381            summary.manufacturer = Some(manufacturer_summary(
382                long_tpl_header
383                    .manufacturer
384                    .as_ref()
385                    .map_or_else(|e| format!("Error: {}", e), |m| m.to_string()),
386            ));
387            summary.access_number = Some(long_tpl_header.short_tpl_header.access_number);
388            summary.status = Some(long_tpl_header.short_tpl_header.status.to_string());
389            summary.security_mode = Some(
390                long_tpl_header
391                    .short_tpl_header
392                    .configuration_field
393                    .security_mode()
394                    .to_string(),
395            );
396            summary.version = Some(long_tpl_header.version);
397            summary.device_type = Some(long_tpl_header.device_type.to_string());
398        }
399        Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
400            short_tpl_header, ..
401        }) => {
402            summary.access_number = Some(short_tpl_header.access_number);
403            summary.status = Some(short_tpl_header.status.to_string());
404            summary.security_mode = Some(
405                short_tpl_header
406                    .configuration_field
407                    .security_mode()
408                    .to_string(),
409            );
410        }
411        Some(UserDataBlock::FixedDataStructure {
412            identification_number,
413            access_number,
414            status,
415            ..
416        }) => {
417            summary.identification_number = Some(identification_number.to_string());
418            summary.access_number = Some(*access_number);
419            summary.status = Some(status.to_string());
420        }
421        _ => {}
422    }
423}
424
425#[cfg(feature = "std")]
426fn finalize_summary(
427    summary: &mut FrameSummary,
428    data_records: Option<&user_data::DataRecords>,
429    user_data: Option<&user_data::UserDataBlock>,
430    encrypted: bool,
431    decrypted: bool,
432) {
433    summary.encrypted = encrypted;
434    summary.decrypted = decrypted;
435    if encrypted && !decrypted {
436        summary.encrypted_payload_hex = variable_data_block(user_data).map(hex_string);
437    } else if let Some(records) = data_records {
438        summary.records = record_summaries(records);
439    }
440}
441
442#[cfg(feature = "std")]
443fn summarize_wired(
444    parsed: &MbusData<frames::WiredFrame>,
445    encrypted: bool,
446    decrypted: bool,
447) -> FrameSummary {
448    let mut summary = match &parsed.frame {
449        frames::WiredFrame::LongFrame { .. } => FrameSummary::new("LongFrame"),
450        frames::WiredFrame::ShortFrame { .. } => FrameSummary::new("ShortFrame"),
451        frames::WiredFrame::ControlFrame { .. } => FrameSummary::new("ControlFrame"),
452        frames::WiredFrame::SingleCharacter { .. } => FrameSummary::new("SingleCharacter"),
453        _ => FrameSummary::new("Unknown"),
454    };
455    match &parsed.frame {
456        frames::WiredFrame::LongFrame {
457            function, address, ..
458        }
459        | frames::WiredFrame::ControlFrame {
460            function, address, ..
461        }
462        | frames::WiredFrame::ShortFrame { function, address } => {
463            summary.function = Some(function.to_string());
464            summary.address = Some(address.to_string());
465        }
466        _ => {}
467    }
468    apply_user_data_summary(&mut summary, parsed.user_data.as_ref());
469    finalize_summary(
470        &mut summary,
471        parsed.data_records.as_ref(),
472        parsed.user_data.as_ref(),
473        encrypted,
474        decrypted,
475    );
476    summary
477}
478
479#[cfg(feature = "std")]
480fn summarize_wireless(
481    parsed: &MbusData<WirelessFrame>,
482    encrypted: bool,
483    decrypted: bool,
484) -> FrameSummary {
485    let manufacturer_id = &parsed.frame.manufacturer_id;
486    let mut summary = FrameSummary::new("Wireless");
487    summary.function = parsed.frame.function.map(|function| function.to_string());
488    summary.identification_number = Some(manufacturer_id.identification_number.to_string());
489    summary.manufacturer = Some(manufacturer_summary(
490        manufacturer_id.manufacturer_code.to_string(),
491    ));
492    summary.version = Some(manufacturer_id.version);
493    summary.device_type = Some(manufacturer_id.device_type.to_string());
494    // A long TPL header overrides the link-layer addressing where present.
495    apply_user_data_summary(&mut summary, parsed.user_data.as_ref());
496    finalize_summary(
497        &mut summary,
498        parsed.data_records.as_ref(),
499        parsed.user_data.as_ref(),
500        encrypted,
501        decrypted,
502    );
503    summary
504}
505
506#[cfg(feature = "std")]
507fn build_output<T: serde::Serialize>(parsed: &T, summary: FrameSummary) -> ParsedOutput {
508    let mut json = serde_json::to_value(parsed).unwrap_or_default();
509    if let serde_json::Value::Object(ref mut map) = json {
510        map.insert(
511            "summary".to_string(),
512            serde_json::to_value(&summary).unwrap_or_default(),
513        );
514    }
515
516    let mut yaml = serde_yaml::to_string(parsed).unwrap_or_default();
517    yaml.push_str(
518        &serde_yaml::to_string(&SummarySection { summary: &summary }).unwrap_or_default(),
519    );
520
521    ParsedOutput {
522        summary,
523        json,
524        yaml,
525    }
526}
527
528/// Parses a frame (wired first, wireless as fallback), applies decryption when
529/// a key is provided, and produces the shared output model every text format
530/// renders from.
531#[cfg(feature = "std")]
532#[cfg_attr(not(feature = "decryption"), allow(unused_mut))]
533fn parse_frame_output(input: &str, key: Option<&[u8; 16]>) -> Option<ParsedOutput> {
534    let data = clean_and_convert(input);
535    #[cfg(feature = "decryption")]
536    let mut decrypted_buffer = [0u8; 256];
537    #[cfg(not(feature = "decryption"))]
538    let _ = key;
539
540    // Try wired first
541    if let Ok(mut parsed) = MbusData::<frames::WiredFrame>::try_from(data.as_slice()) {
542        let encrypted = user_data_is_encrypted(parsed.user_data.as_ref());
543        let mut decrypted = false;
544        #[cfg(feature = "decryption")]
545        if encrypted {
546            if let (Some(key_bytes), Some(user_data)) = (key, parsed.user_data.as_ref()) {
547                if let Some(len) = decrypt_variable_data_with_key(
548                    user_data,
549                    None,
550                    key_bytes,
551                    &mut decrypted_buffer,
552                ) {
553                    if let (
554                        Some(decrypted_data),
555                        Some(user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
556                            long_tpl_header,
557                            ..
558                        }),
559                    ) = (decrypted_buffer.get(..len), &parsed.user_data)
560                    {
561                        parsed.data_records = Some(user_data::DataRecords::new(
562                            decrypted_data,
563                            Some(long_tpl_header),
564                        ));
565                        decrypted = true;
566                    }
567                }
568            }
569        }
570        let summary = summarize_wired(&parsed, encrypted, decrypted);
571        return Some(build_output(&parsed, summary));
572    }
573
574    // If wired fails, try wireless - strip Format A CRCs if present
575    let mut crc_buf = [0u8; 512];
576    let wireless_data =
577        wireless_mbus_link_layer::strip_format_a_crcs(&data, &mut crc_buf).unwrap_or(&data);
578    if let Ok(mut parsed) =
579        MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data)
580    {
581        let encrypted = user_data_is_encrypted(parsed.user_data.as_ref());
582        let mut decrypted = false;
583        #[cfg(feature = "decryption")]
584        if encrypted {
585            if let (Some(key_bytes), Some(user_data)) = (key, parsed.user_data.as_ref()) {
586                if let Some(len) = decrypt_variable_data_with_key(
587                    user_data,
588                    Some(&parsed.frame.manufacturer_id),
589                    key_bytes,
590                    &mut decrypted_buffer,
591                ) {
592                    if let Some(decrypted_data) = decrypted_buffer.get(..len) {
593                        let long_header = match &parsed.user_data {
594                            Some(
595                                user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
596                                    long_tpl_header,
597                                    ..
598                                },
599                            ) => Some(long_tpl_header),
600                            _ => None,
601                        };
602                        parsed.data_records =
603                            Some(user_data::DataRecords::new(decrypted_data, long_header));
604                        decrypted = true;
605                    }
606                }
607            }
608        }
609        let summary = summarize_wireless(&parsed, encrypted, decrypted);
610        return Some(build_output(&parsed, summary));
611    }
612
613    None
614}
615
616#[cfg(feature = "std")]
617#[must_use]
618pub fn parse_to_json(input: &str, key: Option<&[u8; 16]>) -> String {
619    match parse_frame_output(input, key) {
620        Some(output) => serde_json::to_string_pretty(&output.json).unwrap_or_default(),
621        None => "{}".to_string(),
622    }
623}
624
625#[cfg(feature = "std")]
626#[must_use]
627fn parse_to_yaml(input: &str, key: Option<&[u8; 16]>) -> String {
628    match parse_frame_output(input, key) {
629        Some(output) => output.yaml,
630        None => "---\nerror: Could not parse data\n".to_string(),
631    }
632}
633
634#[cfg(all(feature = "std", test))]
635#[must_use]
636fn parse_to_table(input: &str, key: Option<&[u8; 16]>) -> String {
637    match parse_frame_output(input, key) {
638        Some(output) => render_table(&output.summary, key.is_some()),
639        None => "Error: Could not parse data as wired or wireless M-Bus".to_string(),
640    }
641}
642
643#[cfg(all(feature = "std", test))]
644fn render_table(summary: &FrameSummary, key_provided: bool) -> String {
645    let mut out = String::new();
646    let title = match summary.frame_type {
647        "LongFrame" => "Long Frame",
648        "ShortFrame" => "Short Frame",
649        "ControlFrame" => "Control Frame",
650        "SingleCharacter" => "Single Character Frame",
651        "Wireless" => "Wireless Frame",
652        other => other,
653    };
654    out.push_str(title);
655    out.push('\n');
656
657    // Wired frames carry link-layer function and address in their own table.
658    if let (Some(function), Some(address)) = (&summary.function, &summary.address) {
659        let mut table = Table::new();
660        table.set_format(*format::consts::FORMAT_BOX_CHARS);
661        table.set_titles(row!["Function", "Address"]);
662        table.add_row(row![function, address]);
663        out.push_str(&table.to_string());
664    }
665
666    let mut info_table = Table::new();
667    info_table.set_format(*format::consts::FORMAT_BOX_CHARS);
668    info_table.set_titles(row!["Field", "Value"]);
669    if summary.address.is_none() {
670        if let Some(function) = &summary.function {
671            info_table.add_row(row!["Function", function]);
672        }
673    }
674    if let Some(identification_number) = &summary.identification_number {
675        info_table.add_row(row!["Identification Number", identification_number]);
676    }
677    if let Some(manufacturer) = &summary.manufacturer {
678        info_table.add_row(row!["Manufacturer", manufacturer.code]);
679        if let Some(name) = &manufacturer.name {
680            info_table.add_row(row!["Manufacturer Name", name]);
681        }
682        if let Some(website) = &manufacturer.website {
683            info_table.add_row(row!["Website", website]);
684        }
685        if let Some(description) = &manufacturer.description {
686            info_table.add_row(row!["Description", description]);
687        }
688    }
689    if let Some(access_number) = summary.access_number {
690        info_table.add_row(row!["Access Number", access_number]);
691    }
692    if let Some(status) = &summary.status {
693        info_table.add_row(row!["Status", status]);
694    }
695    if let Some(security_mode) = &summary.security_mode {
696        info_table.add_row(row!["Security Mode", security_mode]);
697    }
698    if let Some(version) = summary.version {
699        info_table.add_row(row!["Version", version]);
700    }
701    if let Some(device_type) = &summary.device_type {
702        info_table.add_row(row!["Device Type", device_type]);
703    }
704    if !info_table.is_empty() {
705        out.push_str(&info_table.to_string());
706    }
707
708    if summary.encrypted && !summary.decrypted {
709        if key_provided {
710            out.push_str("Decryption failed\n");
711        }
712        if let Some(payload_hex) = &summary.encrypted_payload_hex {
713            out.push_str("Encrypted Payload : ");
714            out.push_str(payload_hex);
715            out.push('\n');
716        }
717        return out;
718    }
719
720    if summary.decrypted {
721        out.push_str("Decrypted successfully\n");
722    }
723
724    if matches!(summary.frame_type, "LongFrame" | "Wireless") || !summary.records.is_empty() {
725        let mut value_table = Table::new();
726        value_table.set_format(*format::consts::FORMAT_BOX_CHARS);
727        value_table.set_titles(row!["Value", "Data Information", "Header Hex", "Data Hex"]);
728        for record in &summary.records {
729            value_table.add_row(row![
730                record.display,
731                record.data_information,
732                record.header_hex,
733                record.data_hex
734            ]);
735        }
736        out.push_str(&value_table.to_string());
737    }
738
739    out
740}
741
742#[cfg(feature = "std")]
743#[must_use]
744pub fn parse_to_csv(input: &str, key: Option<&[u8; 16]>) -> String {
745    use prettytable::csv;
746
747    let mut writer = csv::Writer::from_writer(vec![]);
748
749    let Some(output) = parse_frame_output(input, key) else {
750        writer
751            .write_record(["Error"])
752            .map_err(|_| ())
753            .unwrap_or_default();
754        writer
755            .write_record(["Error parsing data as wired or wireless M-Bus"])
756            .map_err(|_| ())
757            .unwrap_or_default();
758        let csv_data = writer.into_inner().unwrap_or_default();
759        return String::from_utf8(csv_data)
760            .unwrap_or_else(|_| "Error converting CSV data to string".to_string());
761    };
762
763    let summary = &output.summary;
764
765    let mut headers = vec![
766        "FrameType".to_string(),
767        "Function".to_string(),
768        "Address".to_string(),
769        "Identification Number".to_string(),
770        "Manufacturer".to_string(),
771        "Manufacturer Name".to_string(),
772        "Access Number".to_string(),
773        "Status".to_string(),
774        "Security Mode".to_string(),
775        "Version".to_string(),
776        "Device Type".to_string(),
777    ];
778    for i in 1..=summary.records.len() {
779        headers.push(format!("DataPoint{}_Value", i));
780        headers.push(format!("DataPoint{}_Info", i));
781    }
782    writer
783        .write_record(&headers)
784        .map_err(|_| ())
785        .unwrap_or_default();
786
787    let mut row = vec![
788        summary.frame_type.to_string(),
789        summary.function.clone().unwrap_or_default(),
790        summary.address.clone().unwrap_or_default(),
791        summary.identification_number.clone().unwrap_or_default(),
792        summary
793            .manufacturer
794            .as_ref()
795            .map(|m| m.code.clone())
796            .unwrap_or_default(),
797        summary
798            .manufacturer
799            .as_ref()
800            .and_then(|m| m.name.clone())
801            .unwrap_or_default(),
802        summary
803            .access_number
804            .map(|n| n.to_string())
805            .unwrap_or_default(),
806        summary.status.clone().unwrap_or_default(),
807        summary.security_mode.clone().unwrap_or_default(),
808        summary.version.map(|v| v.to_string()).unwrap_or_default(),
809        summary.device_type.clone().unwrap_or_default(),
810    ];
811    for record in &summary.records {
812        row.push(record.display.clone());
813        row.push(record.data_information.clone());
814    }
815    writer
816        .write_record(&row)
817        .map_err(|_| ())
818        .unwrap_or_default();
819
820    let csv_data = writer.into_inner().unwrap_or_default();
821    String::from_utf8(csv_data)
822        .unwrap_or_else(|_| "Error converting CSV data to string".to_string())
823}
824
825#[cfg(feature = "std")]
826#[must_use]
827pub fn parse_to_mermaid(input: &str, _key: Option<&[u8; 16]>) -> String {
828    use user_data::UserDataBlock;
829
830    const MAX_PER_ROW: usize = 4;
831    // Colors for data record nodes (fill, text), cycling through the palette
832    const RECORD_COLORS: &[(&str, &str)] = &[
833        ("#1565c0", "#fff"),
834        ("#2e7d32", "#fff"),
835        ("#e65100", "#fff"),
836        ("#6a1b9a", "#fff"),
837        ("#c62828", "#fff"),
838        ("#00695c", "#fff"),
839        ("#f9a825", "#000"),
840        ("#4527a0", "#fff"),
841    ];
842
843    let data = clean_and_convert(input);
844
845    // Try wired first
846    if let Ok(parsed_data) = MbusData::<frames::WiredFrame>::try_from(data.as_slice()) {
847        let mut out = String::from("flowchart TD\n");
848        let mut styles = String::new();
849
850        match parsed_data.frame {
851            frames::WiredFrame::LongFrame {
852                function,
853                address,
854                data: _,
855            } => {
856                // Frame header subgraph
857                out.push_str("    subgraph FRAME_SG[\"Frame Header\"]\n");
858                out.push_str("");
859                out.push_str(&format!(
860                    "        FTYPE[\"Long Frame\"]\n        FUNC[\"Function: {}\"]\n        ADDR[\"Address: {}\"]\n",
861                    mermaid_escape(&format!("{}", function)),
862                    mermaid_escape(&format!("{}", address))
863                ));
864                let (chains, pads) =
865                    mermaid_centered_chains(&["FTYPE", "FUNC", "ADDR"], MAX_PER_ROW, "FP");
866                out.push_str(&chains);
867                out.push_str("    end\n");
868                styles.push_str(&pads);
869                styles.push_str("    style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
870                styles.push_str("    style FTYPE fill:#2980b9,color:#fff,stroke:#1a5276\n");
871                styles.push_str("    style FUNC fill:#2980b9,color:#fff,stroke:#1a5276\n");
872                styles.push_str("    style ADDR fill:#2980b9,color:#fff,stroke:#1a5276\n");
873
874                if let Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
875                    long_tpl_header,
876                    ..
877                }) = &parsed_data.user_data
878                {
879                    let mfr = long_tpl_header
880                        .manufacturer
881                        .as_ref()
882                        .map_or_else(|e| format!("Error: {}", e), |m| format!("{}", m));
883
884                    let mfr_info = crate::manufacturers::lookup_manufacturer(&mfr);
885                    out.push_str("    subgraph DEV_SG[\"Device Info\"]\n");
886                    out.push_str("");
887                    out.push_str(&format!(
888                        "        DEV1[\"ID: {}\"]\n",
889                        mermaid_escape(&format!("{}", long_tpl_header.identification_number))
890                    ));
891                    out.push_str(&format!(
892                        "        DEV2[\"Manufacturer: {}\"]\n",
893                        mermaid_escape(&mfr)
894                    ));
895                    out.push_str(&format!(
896                        "        DEV3[\"Version: {}\"]\n",
897                        long_tpl_header.version
898                    ));
899                    out.push_str(&format!(
900                        "        DEV4[\"Device Type: {}\"]\n",
901                        mermaid_escape(&format!("{:?}", long_tpl_header.device_type))
902                    ));
903                    out.push_str(&format!(
904                        "        DEV5[\"Access Number: {}\"]\n",
905                        long_tpl_header.short_tpl_header.access_number
906                    ));
907                    out.push_str(&format!(
908                        "        DEV6[\"Status: {}\"]\n",
909                        mermaid_escape(&format!("{}", long_tpl_header.short_tpl_header.status))
910                    ));
911                    let mut dev_node_count = 6usize;
912                    if let Some(ref info) = mfr_info {
913                        dev_node_count += 1;
914                        out.push_str(&format!(
915                            "        DEV{}[\"Name: {}\"]\n",
916                            dev_node_count,
917                            mermaid_escape(info.name)
918                        ));
919                        dev_node_count += 1;
920                        out.push_str(&format!(
921                            "        DEV{}[\"Website: {}\"]\n",
922                            dev_node_count,
923                            mermaid_escape(info.website)
924                        ));
925                        dev_node_count += 1;
926                        out.push_str(&format!(
927                            "        DEV{}[\"{}\"]\n",
928                            dev_node_count,
929                            mermaid_escape(info.description)
930                        ));
931                    }
932                    let dev_ids: Vec<String> =
933                        (1..=dev_node_count).map(|i| format!("DEV{}", i)).collect();
934                    let dev_id_refs: Vec<&str> = dev_ids.iter().map(|s| s.as_str()).collect();
935                    let (chains, pads) = mermaid_centered_chains(&dev_id_refs, MAX_PER_ROW, "DP");
936                    out.push_str(&chains);
937                    out.push_str("    end\n");
938                    styles.push_str(&pads);
939                    styles.push_str("    style DEV_SG fill:#1e8449,color:#fff,stroke:#145a32\n");
940                    for i in 1..=dev_node_count {
941                        styles.push_str(&format!(
942                            "    style DEV{} fill:#27ae60,color:#fff,stroke:#145a32\n",
943                            i
944                        ));
945                    }
946
947                    out.push_str("    FRAME_SG --> DEV_SG\n");
948                }
949
950                if let Some(data_records) = parsed_data.data_records {
951                    out.push_str("    subgraph REC_SG[\"Data Records\"]\n");
952                    out.push_str("");
953                    let records: Vec<_> = data_records.flatten().collect();
954                    for (i, record) in records.iter().enumerate() {
955                        let value_information = match record
956                            .data_record_header
957                            .processed_data_record_header
958                            .value_information
959                        {
960                            Some(ref x) => format!("{}", x),
961                            None => String::new(),
962                        };
963                        let label = format!("({}{}", record.data, value_information);
964                        out.push_str(&format!("        R{}[\"{}\"]\n", i, mermaid_escape(&label)));
965                        let (fill, text) = RECORD_COLORS
966                            .get(i % RECORD_COLORS.len())
967                            .copied()
968                            .unwrap_or(("#888", "#fff"));
969                        styles.push_str(&format!(
970                            "    style R{} fill:{},color:{},stroke:#333\n",
971                            i, fill, text
972                        ));
973                    }
974                    let ids: Vec<String> = (0..records.len()).map(|i| format!("R{}", i)).collect();
975                    let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
976                    let (chains, pads) = mermaid_centered_chains(&id_refs, MAX_PER_ROW, "RP");
977                    out.push_str(&chains);
978                    out.push_str("    end\n");
979                    styles.push_str("    style REC_SG fill:#6c3483,color:#fff,stroke:#4a235a\n");
980                    styles.push_str(&pads);
981                    out.push_str("    DEV_SG --> REC_SG\n");
982                }
983            }
984            frames::WiredFrame::ShortFrame { function, address } => {
985                out.push_str("    subgraph FRAME_SG[\"Short Frame\"]\n");
986                out.push_str(&format!(
987                    "        FUNC[\"Function: {}\"]\n        ADDR[\"Address: {}\"]\n",
988                    mermaid_escape(&format!("{}", function)),
989                    mermaid_escape(&format!("{}", address))
990                ));
991                out.push_str("    end\n");
992                styles.push_str("    style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
993            }
994            frames::WiredFrame::SingleCharacter { character } => {
995                out.push_str(&format!(
996                    "    FRAME[\"Single Character: 0x{:02X}\"]\n",
997                    character
998                ));
999                styles.push_str("    style FRAME fill:#2e86c1,color:#fff,stroke:#1a5276\n");
1000            }
1001            frames::WiredFrame::ControlFrame {
1002                function, address, ..
1003            } => {
1004                out.push_str("    subgraph FRAME_SG[\"Control Frame\"]\n");
1005                out.push_str(&format!(
1006                    "        FUNC[\"Function: {}\"]\n        ADDR[\"Address: {}\"]\n",
1007                    mermaid_escape(&format!("{}", function)),
1008                    mermaid_escape(&format!("{}", address))
1009                ));
1010                out.push_str("    end\n");
1011                styles.push_str("    style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
1012            }
1013            _ => {
1014                out.push_str("    FRAME[\"Unknown Frame\"]\n");
1015            }
1016        }
1017        out.push_str(&styles);
1018        return out;
1019    }
1020
1021    // Try wireless
1022    let mut crc_buf = [0u8; 512];
1023    let wireless_data =
1024        wireless_mbus_link_layer::strip_format_a_crcs(&data, &mut crc_buf).unwrap_or(&data);
1025    if let Ok(parsed_data) =
1026        MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data)
1027    {
1028        let wireless_mbus_link_layer::WirelessFrame {
1029            function,
1030            manufacturer_id,
1031            ..
1032        } = &parsed_data.frame;
1033
1034        let mut out = String::from("flowchart TD\n");
1035        let mut styles = String::new();
1036
1037        let wmfr_str = format!("{}", manufacturer_id.manufacturer_code);
1038        let wmfr_info = crate::manufacturers::lookup_manufacturer(&wmfr_str);
1039        out.push_str("    subgraph FRAME_SG[\"Wireless Frame\"]\n");
1040        out.push_str(&format!("        FUNC[\"Function: {:?}\"]\n", function));
1041        out.push_str(&format!(
1042            "        MFR[\"Manufacturer: {}\"]\n",
1043            mermaid_escape(&wmfr_str)
1044        ));
1045        out.push_str(&format!(
1046            "        ID[\"ID: {:?}\"]\n",
1047            manufacturer_id.identification_number
1048        ));
1049        out.push_str(&format!(
1050            "        DEVT[\"Device Type: {:?}\"]\n",
1051            manufacturer_id.device_type
1052        ));
1053        out.push_str(&format!(
1054            "        VER[\"Version: {:?}\"]\n",
1055            manufacturer_id.version
1056        ));
1057        let mut wframe_nodes: Vec<&str> = vec!["FUNC", "MFR", "ID", "DEVT", "VER"];
1058        if let Some(ref info) = wmfr_info {
1059            out.push_str(&format!(
1060                "        MFRNAME[\"Name: {}\"]\n",
1061                mermaid_escape(info.name)
1062            ));
1063            out.push_str(&format!(
1064                "        MFRWEB[\"Website: {}\"]\n",
1065                mermaid_escape(info.website)
1066            ));
1067            out.push_str(&format!(
1068                "        MFRDESC[\"{}\"]\n",
1069                mermaid_escape(info.description)
1070            ));
1071            wframe_nodes.extend_from_slice(&["MFRNAME", "MFRWEB", "MFRDESC"]);
1072        }
1073        out.push_str("    end\n");
1074        styles.push_str("    style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
1075        for node in &wframe_nodes {
1076            styles.push_str(&format!(
1077                "    style {} fill:#2980b9,color:#fff,stroke:#1a5276\n",
1078                node
1079            ));
1080        }
1081
1082        if let Some(data_records) = parsed_data.data_records {
1083            out.push_str("    subgraph REC_SG[\"Data Records\"]\n");
1084            let records: Vec<_> = data_records.flatten().collect();
1085            for (i, record) in records.iter().enumerate() {
1086                let value_information = match record
1087                    .data_record_header
1088                    .processed_data_record_header
1089                    .value_information
1090                {
1091                    Some(ref x) => format!("{}", x),
1092                    None => String::new(),
1093                };
1094                let label = format!("({}{}", record.data, value_information);
1095                out.push_str(&format!("        R{}[\"{}\"]\n", i, mermaid_escape(&label)));
1096                let (fill, text) = RECORD_COLORS
1097                    .get(i % RECORD_COLORS.len())
1098                    .copied()
1099                    .unwrap_or(("#888", "#fff"));
1100                styles.push_str(&format!(
1101                    "    style R{} fill:{},color:{},stroke:#333\n",
1102                    i, fill, text
1103                ));
1104            }
1105            out.push_str("    end\n");
1106            styles.push_str("    style REC_SG fill:#6c3483,color:#fff,stroke:#4a235a\n");
1107            out.push_str("    FRAME_SG --> REC_SG\n");
1108        }
1109
1110        out.push_str(&styles);
1111        return out;
1112    }
1113
1114    "flowchart TD\n    ERR[\"Error: Could not parse data\"]\n".to_string()
1115}
1116
1117/// Returns (body, styles) where body contains padding node declarations + chain lines,
1118/// and styles contains the invisible styles for padding nodes.
1119/// Pads each incomplete row symmetrically so nodes appear centred.
1120#[cfg(feature = "std")]
1121fn mermaid_centered_chains(ids: &[&str], max_per_row: usize, pad_prefix: &str) -> (String, String) {
1122    let mut body = String::new();
1123    let mut styles = String::new();
1124    let mut pad_idx = 0usize;
1125    for chunk in ids.chunks(max_per_row) {
1126        let row: Vec<String> = if chunk.len() == max_per_row {
1127            chunk.iter().map(|s| s.to_string()).collect()
1128        } else {
1129            let padding = max_per_row - chunk.len();
1130            let left = padding / 2;
1131            let right = padding - left;
1132            let mut row: Vec<String> = Vec::new();
1133            for _ in 0..left {
1134                let id = format!("{}P{}", pad_prefix, pad_idx);
1135                body.push_str(&format!("        {}[\" \"]\n", id));
1136                styles.push_str(&format!(
1137                    "    style {} fill:none,stroke:none,color:none\n",
1138                    id
1139                ));
1140                row.push(id);
1141                pad_idx += 1;
1142            }
1143            row.extend(chunk.iter().map(|s| s.to_string()));
1144            for _ in 0..right {
1145                let id = format!("{}P{}", pad_prefix, pad_idx);
1146                body.push_str(&format!("        {}[\" \"]\n", id));
1147                styles.push_str(&format!(
1148                    "    style {} fill:none,stroke:none,color:none\n",
1149                    id
1150                ));
1151                row.push(id);
1152                pad_idx += 1;
1153            }
1154            row
1155        };
1156        // Emit individual pairs instead of a chain to maximise mermaid compatibility
1157        for pair in row.windows(2) {
1158            if let (Some(a), Some(b)) = (pair.first(), pair.get(1)) {
1159                body.push_str(&format!("        {}~~~{}\n", a, b));
1160            }
1161        }
1162    }
1163    (body, styles)
1164}
1165
1166#[cfg(feature = "std")]
1167#[must_use]
1168fn parse_to_annotated(input: &str) -> String {
1169    let data = clean_and_convert(input);
1170    annotated_segments_to_json(&data)
1171}
1172
1173#[cfg(feature = "std")]
1174#[must_use]
1175fn parse_to_hexview(input: &str, key: Option<&[u8; 16]>) -> String {
1176    let data = clean_and_convert(input);
1177
1178    #[cfg(feature = "decryption")]
1179    if let Some(key_bytes) = key {
1180        if let Some(display_data) = decrypted_hexview_data(&data, key_bytes) {
1181            return match crate::annotate::annotate_frame(&display_data) {
1182                Ok(mut segments) => {
1183                    replace_encrypted_payload_segments(&mut segments, &display_data);
1184                    serde_json::to_string_pretty(&serde_json::json!({
1185                        "bytes": display_data,
1186                        "segments": segments,
1187                        "decrypted": true,
1188                    }))
1189                    .unwrap_or_default()
1190                }
1191                Err(e) => format!("{{\"error\": \"{}\"}}", e),
1192            };
1193        }
1194    }
1195
1196    #[cfg(not(feature = "decryption"))]
1197    let _ = key;
1198
1199    annotated_segments_to_json(&data)
1200}
1201
1202#[cfg(feature = "std")]
1203pub(crate) fn render_annotated_bytes(
1204    data: &[u8],
1205    key: Option<&[u8; 16]>,
1206) -> Result<String, crate::output::OutputError> {
1207    let original_segments = crate::annotate::annotate_frame(data).map_err(|error| {
1208        crate::output::OutputError::Rendering {
1209            code: "annotation.failed",
1210            message: error.to_string(),
1211        }
1212    })?;
1213
1214    #[cfg(feature = "decryption")]
1215    if let Some(key_bytes) = key {
1216        if let Some(display_data) = decrypted_hexview_data(data, key_bytes) {
1217            let mut display_segments =
1218                crate::annotate::annotate_frame(&display_data).map_err(|error| {
1219                    crate::output::OutputError::Rendering {
1220                        code: "annotation.failed",
1221                        message: error.to_string(),
1222                    }
1223                })?;
1224            replace_encrypted_payload_segments(&mut display_segments, &display_data);
1225            return serde_json::to_string_pretty(&serde_json::json!({
1226                "schema_version": 3,
1227                "bytes": display_data,
1228                "segments": display_segments,
1229                "decrypted": true,
1230                "original": {
1231                    "bytes": data,
1232                    "segments": original_segments,
1233                }
1234            }))
1235            .map_err(|error| crate::output::OutputError::Serialization {
1236                format: "annotated",
1237                message: error.to_string(),
1238            });
1239        }
1240    }
1241
1242    #[cfg(not(feature = "decryption"))]
1243    let _ = key;
1244
1245    serde_json::to_string_pretty(&serde_json::json!({
1246        "schema_version": 3,
1247        "bytes": data,
1248        "segments": original_segments,
1249        "decrypted": false,
1250    }))
1251    .map_err(|error| crate::output::OutputError::Serialization {
1252        format: "annotated",
1253        message: error.to_string(),
1254    })
1255}
1256
1257#[cfg(feature = "std")]
1258#[must_use]
1259fn annotated_segments_to_json(data: &[u8]) -> String {
1260    match crate::annotate::annotate_frame(data) {
1261        Ok(segments) => serde_json::to_string_pretty(&segments).unwrap_or_default(),
1262        Err(e) => format!("{{\"error\": \"{}\"}}", e),
1263    }
1264}
1265
1266#[cfg(all(feature = "std", feature = "decryption"))]
1267fn decrypted_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1268    decrypted_wired_hexview_data(data, key).or_else(|| decrypted_wireless_hexview_data(data, key))
1269}
1270
1271#[cfg(all(feature = "std", feature = "decryption"))]
1272fn replace_encrypted_payload_segments(
1273    segments: &mut Vec<crate::annotate::ByteSegment>,
1274    display_data: &[u8],
1275) {
1276    let mut rewritten = Vec::with_capacity(segments.len());
1277
1278    for segment in segments.drain(..) {
1279        if segment.kind == crate::annotate::SegmentKind::EncryptedPayload {
1280            let before_len = rewritten.len();
1281            if let Some(data) = display_data.get(segment.start..segment.end) {
1282                crate::annotate::annotate_data_records(&mut rewritten, segment.start, data);
1283            }
1284            if rewritten.len() == before_len {
1285                rewritten.push(segment);
1286            }
1287        } else {
1288            rewritten.push(segment);
1289        }
1290    }
1291
1292    *segments = rewritten;
1293}
1294
1295#[cfg(all(feature = "std", feature = "decryption"))]
1296fn decrypted_wired_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1297    let parsed_data = MbusData::<frames::WiredFrame>::try_from(data).ok()?;
1298    let user_data = parsed_data.user_data.as_ref()?;
1299    let variable_data_len = user_data.variable_data_len();
1300    if variable_data_len == 0 {
1301        return None;
1302    }
1303
1304    let mut decrypted_buffer = [0u8; 256];
1305    let decrypted_len =
1306        decrypt_variable_data_with_key(user_data, None, key, &mut decrypted_buffer)?;
1307
1308    let user_data_len = match parsed_data.frame {
1309        frames::WiredFrame::LongFrame {
1310            data: user_data_slice,
1311            ..
1312        }
1313        | frames::WiredFrame::ControlFrame {
1314            data: user_data_slice,
1315            ..
1316        } => user_data_slice.len(),
1317        _ => return None,
1318    };
1319
1320    let payload_start = 6 + user_data_len.checked_sub(variable_data_len)?;
1321    let payload_end = payload_start + variable_data_len;
1322    let mut display_data = data.to_vec();
1323    let decrypted_data = decrypted_buffer.get(..decrypted_len)?;
1324    display_data.splice(payload_start..payload_end, decrypted_data.iter().copied());
1325
1326    let length = display_data.len().checked_sub(6)?;
1327    if length > u8::MAX as usize || display_data.len() < 6 {
1328        return None;
1329    }
1330    *display_data.get_mut(1)? = length as u8;
1331    *display_data.get_mut(2)? = length as u8;
1332
1333    let checksum_index = display_data.len().checked_sub(2)?;
1334    let checksum = display_data
1335        .get(4..checksum_index)?
1336        .iter()
1337        .fold(0u8, |acc, byte| acc.wrapping_add(*byte));
1338    *display_data.get_mut(checksum_index)? = checksum;
1339
1340    Some(display_data)
1341}
1342
1343#[cfg(all(feature = "std", feature = "decryption"))]
1344fn decrypted_wireless_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1345    let mut crc_buf = [0u8; 512];
1346    let wireless_data =
1347        wireless_mbus_link_layer::strip_format_a_crcs(data, &mut crc_buf).unwrap_or(data);
1348    let parsed_data =
1349        MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data).ok()?;
1350    let user_data = parsed_data.user_data.as_ref()?;
1351    let variable_data_len = user_data.variable_data_len();
1352    if variable_data_len == 0 {
1353        return None;
1354    }
1355
1356    let mut decrypted_buffer = [0u8; 256];
1357    let manufacturer_id = &parsed_data.frame.manufacturer_id;
1358    let decrypted_len = decrypt_variable_data_with_key(
1359        user_data,
1360        Some(manufacturer_id),
1361        key,
1362        &mut decrypted_buffer,
1363    )?;
1364
1365    let app_data_len = parsed_data.frame.data.len();
1366    let app_start = wireless_data.len().checked_sub(app_data_len)?;
1367    let payload_start = app_start + app_data_len.checked_sub(variable_data_len)?;
1368    let payload_end = payload_start + variable_data_len;
1369
1370    let mut display_data = wireless_data.to_vec();
1371    let decrypted_data = decrypted_buffer.get(..decrypted_len)?;
1372    display_data.splice(payload_start..payload_end, decrypted_data.iter().copied());
1373    if let Some(length) = display_data.len().checked_sub(1) {
1374        if length <= u8::MAX as usize {
1375            if let Some(length_byte) = display_data.get_mut(0) {
1376                *length_byte = length as u8;
1377            }
1378        }
1379    }
1380
1381    Some(display_data)
1382}
1383
1384#[cfg(all(feature = "std", feature = "decryption"))]
1385fn decrypt_variable_data_with_key(
1386    user_data: &user_data::UserDataBlock<'_>,
1387    wireless_manufacturer_id: Option<&wireless_mbus_link_layer::ManufacturerId>,
1388    key: &[u8; 16],
1389    output: &mut [u8],
1390) -> Option<usize> {
1391    use user_data::UserDataBlock;
1392
1393    let mut provider = crate::decryption::StaticKeyProvider::<1>::new();
1394    match user_data {
1395        UserDataBlock::VariableDataStructureWithLongTplHeader {
1396            long_tpl_header, ..
1397        } => {
1398            if !long_tpl_header.is_encrypted() {
1399                return None;
1400            }
1401            let manufacturer = long_tpl_header.manufacturer.as_ref().ok()?;
1402            provider
1403                .add_key(
1404                    manufacturer.to_id(),
1405                    long_tpl_header.identification_number.number,
1406                    *key,
1407                )
1408                .ok()?;
1409            user_data.decrypt_variable_data(&provider, output).ok()
1410        }
1411        UserDataBlock::VariableDataStructureWithShortTplHeader {
1412            short_tpl_header, ..
1413        } => {
1414            if !short_tpl_header.is_encrypted() {
1415                return None;
1416            }
1417            let manufacturer_id = wireless_manufacturer_id?;
1418            provider
1419                .add_key(
1420                    manufacturer_id.manufacturer_code.to_id(),
1421                    manufacturer_id.identification_number.number,
1422                    *key,
1423                )
1424                .ok()?;
1425            user_data
1426                .decrypt_variable_data_with_context(
1427                    &provider,
1428                    manufacturer_id.manufacturer_code,
1429                    manufacturer_id.identification_number.number,
1430                    manufacturer_id.version,
1431                    manufacturer_id.device_type,
1432                    output,
1433                )
1434                .ok()
1435        }
1436        _ => None,
1437    }
1438}
1439
1440#[cfg(feature = "std")]
1441fn mermaid_escape(s: &str) -> String {
1442    s.replace('"', "#quot;")
1443        .replace('[', "#91;")
1444        .replace(']', "#93;")
1445}
1446
1447#[cfg(test)]
1448mod tests {
1449
1450    #[cfg(feature = "std")]
1451    #[test]
1452    fn test_csv_converter() {
1453        use super::parse_to_csv;
1454        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1455        let csv_output: String = parse_to_csv(input, None);
1456        println!("{}", csv_output);
1457        let yaml_output: String = super::parse_to_yaml(input, None);
1458        println!("{}", yaml_output);
1459        let json_output: String = super::parse_to_json(input, None);
1460        println!("{}", json_output);
1461        let table_output: String = super::parse_to_table(input, None);
1462        println!("{}", table_output);
1463    }
1464
1465    #[cfg(feature = "std")]
1466    #[test]
1467    fn test_csv_expected_output() {
1468        use super::parse_to_csv;
1469        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1470        let csv_output = parse_to_csv(input, None);
1471
1472        let expected = "FrameType,Function,Address,Identification Number,Manufacturer,Manufacturer Name,Access Number,Status,Security Mode,Version,Device Type,DataPoint1_Value,DataPoint1_Info,DataPoint2_Value,DataPoint2_Info,DataPoint3_Value,DataPoint3_Info,DataPoint4_Value,DataPoint4_Info,DataPoint5_Value,DataPoint5_Info,DataPoint6_Value,DataPoint6_Info,DataPoint7_Value,DataPoint7_Info,DataPoint8_Value,DataPoint8_Info,DataPoint9_Value,DataPoint9_Info,DataPoint10_Value,DataPoint10_Info\nLongFrame,\"RspUd (ACD: false, DFC: false)\",Primary (1),02205100,SLB,Schlumberger Industries,0,\"Permanent error, Manufacturer specific 3\",No encryption used,2,Heat Meter (Return),(0)e4[Wh](Energy),\"0,Inst,32-bit Integer\",(3)e-1[m³](Volume),\"0,Inst,BCD 8-digit\",(0)e3[W](Power),\"0,Inst,BCD 6-digit\",(0)e-3[m³h⁻¹](VolumeFlow),\"0,Inst,BCD 6-digit\",(1288)e-1[°C](FlowTemperature),\"0,Inst,BCD 4-digit\",(516)e-1[°C](ReturnTemperature),\"0,Inst,BCD 4-digit\",(7723)e-2[°K](TemperatureDifference),\"0,Inst,BCD 6-digit\",(12/Jan/2012)(Date),\"0,Inst,Date Type G\",(3383)[day](OperatingTime),\"0,Inst,16-bit Integer\",\"(Manufacturer Specific: [96, 0])\",\"0,Inst,Special Functions (ManufacturerSpecific)\"\n";
1473
1474        assert_eq!(csv_output, expected);
1475    }
1476
1477    #[cfg(feature = "std")]
1478    #[test]
1479    fn test_wireless_csv_contains_header_info() {
1480        // Wireless frame with a short TPL header: the identification number,
1481        // manufacturer, version and device type come from the link layer and
1482        // must not be missing from the CSV output.
1483        let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1484        let csv_output = super::parse_to_csv(input, None);
1485
1486        let mut lines = csv_output.lines();
1487        let header: Vec<&str> = lines.next().expect("header row").split(',').collect();
1488        let row = lines.next().expect("data row");
1489        // Parse the row respecting quoting via a simple check of key fields
1490        assert!(header.starts_with(&[
1491            "FrameType",
1492            "Function",
1493            "Address",
1494            "Identification Number",
1495            "Manufacturer",
1496            "Manufacturer Name",
1497            "Access Number",
1498            "Status",
1499            "Security Mode",
1500            "Version",
1501            "Device Type"
1502        ]));
1503        assert!(row.starts_with("Wireless,SndNr,,12345678,ELS,Elster Group,42,No Error(s),"));
1504        assert!(row.contains("AES-CBC-128; IV ≠ 0"));
1505        assert!(row.contains(",51,Gas Meter"));
1506    }
1507
1508    #[cfg(feature = "std")]
1509    #[test]
1510    fn test_wireless_encrypted_csv_without_key_has_no_garbage_records() {
1511        let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1512        let csv_output = super::parse_to_csv(input, None);
1513        assert!(!csv_output.contains("DataPoint1_Value"));
1514        // Header info must still be present even though the payload is encrypted
1515        assert!(csv_output.contains("12345678"));
1516    }
1517
1518    #[cfg(all(feature = "std", feature = "decryption"))]
1519    #[test]
1520    fn test_wireless_csv_with_key_contains_records() {
1521        let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1522        let key = [
1523            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1524            0x0F, 0x11,
1525        ];
1526        let csv_output = super::parse_to_csv(input, Some(&key));
1527        assert!(csv_output.contains("12345678"));
1528        assert!(csv_output.contains("(2850427)e-2[m³](Volume)"));
1529    }
1530
1531    #[cfg(feature = "std")]
1532    #[test]
1533    fn test_json_and_yaml_contain_summary() {
1534        let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1535        let json_output = super::parse_to_json(input, None);
1536        let parsed: serde_json::Value =
1537            serde_json::from_str(&json_output).expect("json output should be valid");
1538        let summary = parsed.get("summary").expect("json should contain summary");
1539        assert_eq!(
1540            summary.get("frame_type").and_then(|v| v.as_str()),
1541            Some("Wireless")
1542        );
1543        assert_eq!(
1544            summary
1545                .get("identification_number")
1546                .and_then(|v| v.as_str()),
1547            Some("12345678")
1548        );
1549        assert_eq!(
1550            summary
1551                .get("manufacturer")
1552                .and_then(|m| m.get("code"))
1553                .and_then(|v| v.as_str()),
1554            Some("ELS")
1555        );
1556        assert_eq!(
1557            summary.get("security_mode").and_then(|v| v.as_str()),
1558            Some("AES-CBC-128; IV ≠ 0")
1559        );
1560        assert_eq!(
1561            summary.get("status").and_then(|v| v.as_str()),
1562            Some("No Error(s)")
1563        );
1564        assert_eq!(
1565            summary.get("encrypted").and_then(|v| v.as_bool()),
1566            Some(true)
1567        );
1568
1569        let yaml_output = super::parse_to_yaml(input, None);
1570        assert!(yaml_output.contains("summary:"));
1571        assert!(yaml_output.contains("frame_type: Wireless"));
1572        assert!(yaml_output.contains("security_mode: AES-CBC-128; IV ≠ 0"));
1573    }
1574
1575    #[cfg(feature = "std")]
1576    #[test]
1577    fn test_all_formats_agree_on_header_fields() {
1578        // The same header information must be present in every output format.
1579        let inputs = [
1580            // wired long frame
1581            "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16",
1582            // wireless with short TPL header
1583            "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A",
1584            // wireless without TPL header (ELL)
1585            "1444AE0C7856341201078C2027780B134365877AC5",
1586        ];
1587        for input in inputs {
1588            let output = super::parse_frame_output(input, None).expect("frame should parse");
1589            let summary = &output.summary;
1590            let table = super::parse_to_table(input, None);
1591            let csv = super::parse_to_csv(input, None);
1592            let json = super::parse_to_json(input, None);
1593            let yaml = super::parse_to_yaml(input, None);
1594            for value in [
1595                summary.identification_number.clone(),
1596                summary.manufacturer.as_ref().map(|m| m.code.clone()),
1597                summary.status.clone(),
1598                summary.security_mode.clone(),
1599                summary.device_type.clone(),
1600            ]
1601            .into_iter()
1602            .flatten()
1603            {
1604                for (format, output) in [
1605                    ("table", &table),
1606                    ("csv", &csv),
1607                    ("json", &json),
1608                    ("yaml", &yaml),
1609                ] {
1610                    assert!(
1611                        output.contains(&value),
1612                        "{format} output is missing header value {value:?} for input {input}"
1613                    );
1614                }
1615            }
1616
1617            // The structured record fields in the JSON summary must agree with
1618            // the normalized summary the table and CSV render from.
1619            let parsed: serde_json::Value =
1620                serde_json::from_str(&json).expect("json output should be valid");
1621            let json_records = parsed
1622                .get("summary")
1623                .and_then(|s| s.get("records"))
1624                .and_then(|r| r.as_array())
1625                .expect("json summary should contain records");
1626            assert_eq!(json_records.len(), summary.records.len());
1627            for (json_record, record) in json_records.iter().zip(&summary.records) {
1628                assert_eq!(
1629                    json_record.get("display").and_then(|v| v.as_str()),
1630                    Some(record.display.as_str()),
1631                    "json record display mismatch for input {input}"
1632                );
1633                assert_eq!(
1634                    json_record.get("value").and_then(|v| v.as_f64()),
1635                    record.value,
1636                    "json record value mismatch for input {input}"
1637                );
1638                assert_eq!(
1639                    json_record
1640                        .get("exponent")
1641                        .and_then(|v| v.as_i64())
1642                        .map(|v| v as isize),
1643                    record.exponent,
1644                    "json record exponent mismatch for input {input}"
1645                );
1646                assert_eq!(
1647                    json_record.get("unit").and_then(|v| v.as_str()),
1648                    record.unit.as_deref(),
1649                    "json record unit mismatch for input {input}"
1650                );
1651                assert_eq!(
1652                    json_record.get("quantity").and_then(|v| v.as_str()),
1653                    record.quantity.as_deref(),
1654                    "json record quantity mismatch for input {input}"
1655                );
1656                // The display string the table/CSV formats use must embed the
1657                // same structured information.
1658                if let Some(unit) = &record.unit {
1659                    assert!(
1660                        record.display.contains(unit.as_str()),
1661                        "display {:?} should contain unit {unit:?}",
1662                        record.display
1663                    );
1664                }
1665                if let Some(exponent) = record.exponent.filter(|e| *e != 0) {
1666                    assert!(
1667                        record.display.contains(&format!("e{exponent}")),
1668                        "display {:?} should contain exponent {exponent}",
1669                        record.display
1670                    );
1671                }
1672            }
1673        }
1674    }
1675
1676    #[cfg(feature = "std")]
1677    #[test]
1678    fn test_json_records_contain_structured_values() {
1679        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1680        let json_output = super::parse_to_json(input, None);
1681        let parsed: serde_json::Value =
1682            serde_json::from_str(&json_output).expect("json output should be valid");
1683        let records = parsed
1684            .get("summary")
1685            .and_then(|s| s.get("records"))
1686            .and_then(|r| r.as_array())
1687            .expect("summary should contain records");
1688
1689        // "(3)e-1[m³](Volume)" becomes structured fields.
1690        let volume = records
1691            .iter()
1692            .find(|r| r.get("quantity").and_then(|v| v.as_str()) == Some("Volume"))
1693            .expect("volume record");
1694        assert_eq!(volume.get("value").and_then(|v| v.as_f64()), Some(3.0));
1695        assert_eq!(volume.get("exponent").and_then(|v| v.as_i64()), Some(-1));
1696        assert_eq!(volume.get("unit").and_then(|v| v.as_str()), Some("m³"));
1697        assert_eq!(
1698            volume.get("display").and_then(|v| v.as_str()),
1699            Some("(3)e-1[m³](Volume)")
1700        );
1701
1702        // A date record has no numeric value but keeps its display string.
1703        let date = records
1704            .iter()
1705            .find(|r| r.get("quantity").and_then(|v| v.as_str()) == Some("Date"))
1706            .expect("date record");
1707        assert!(date.get("value").is_none());
1708        assert_eq!(
1709            date.get("display").and_then(|v| v.as_str()),
1710            Some("(12/Jan/2012)(Date)")
1711        );
1712
1713        // A manufacturer-specific record has neither value information nor a
1714        // numeric value.
1715        let manufacturer_specific = records
1716            .iter()
1717            .find(|r| {
1718                r.get("data_information")
1719                    .and_then(|v| v.as_str())
1720                    .is_some_and(|s| s.contains("ManufacturerSpecific"))
1721            })
1722            .expect("manufacturer specific record");
1723        assert!(manufacturer_specific.get("value").is_none());
1724        assert!(manufacturer_specific.get("exponent").is_none());
1725        assert!(manufacturer_specific.get("unit").is_none());
1726        assert!(manufacturer_specific.get("quantity").is_none());
1727    }
1728
1729    #[cfg(feature = "std")]
1730    #[test]
1731    fn test_json_and_yaml_have_no_manufacturer_info() {
1732        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1733        let json_output = super::parse_to_json(input, None);
1734        let parsed: serde_json::Value =
1735            serde_json::from_str(&json_output).expect("json output should be valid");
1736        assert!(parsed.get("manufacturer_info").is_none());
1737        // summary.manufacturer carries the same information instead.
1738        assert_eq!(
1739            parsed
1740                .get("summary")
1741                .and_then(|s| s.get("manufacturer"))
1742                .and_then(|m| m.get("name"))
1743                .and_then(|v| v.as_str()),
1744            Some("Schlumberger Industries")
1745        );
1746
1747        let yaml_output = super::parse_to_yaml(input, None);
1748        assert!(!yaml_output.contains("manufacturer_info:"));
1749        assert!(yaml_output.contains("name: Schlumberger Industries"));
1750    }
1751
1752    #[cfg(feature = "std")]
1753    #[test]
1754    fn test_byte_payloads_serialize_as_hex_strings() {
1755        // Wireless frame: frame.data must be a compact uppercase hex string.
1756        let wireless_input = "1444AE0C7856341201078C2027780B134365877AC5";
1757        let json_output = super::parse_to_json(wireless_input, None);
1758        let parsed: serde_json::Value =
1759            serde_json::from_str(&json_output).expect("json output should be valid");
1760        let frame_data = parsed
1761            .get("frame")
1762            .and_then(|f| f.get("data"))
1763            .expect("wireless frame should contain data");
1764        let hex = frame_data.as_str().expect("frame data should be a string");
1765        assert!(!hex.is_empty());
1766        assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
1767        assert_eq!(hex, hex.to_uppercase());
1768
1769        // Data records: raw_bytes must be hex strings as well.
1770        let wired_input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1771        let json_output = super::parse_to_json(wired_input, None);
1772        let parsed: serde_json::Value =
1773            serde_json::from_str(&json_output).expect("json output should be valid");
1774        let records = parsed
1775            .get("data_records")
1776            .and_then(|r| r.as_array())
1777            .expect("data records");
1778        let first_raw_bytes = records
1779            .first()
1780            .and_then(|r| r.get("raw_bytes"))
1781            .and_then(|v| v.as_str())
1782            .expect("raw_bytes should be a hex string");
1783        assert_eq!(first_raw_bytes, "040700000000");
1784
1785        // Manufacturer-specific data is a hex string instead of a byte array.
1786        assert!(json_output.contains("\"ManufacturerSpecific\": \"6000\""));
1787    }
1788
1789    #[cfg(feature = "std")]
1790    #[test]
1791    fn test_yaml_expected_output() {
1792        use super::parse_to_yaml;
1793        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1794        let yaml_output = parse_to_yaml(input, None);
1795
1796        // First line of YAML output to test against - we'll test just the beginning to avoid a massive string
1797        let expected_start = "frame: !LongFrame\n  function: !RspUd\n    acd: false\n    dfc: false\n  address: !Primary 1\nuser_data: !VariableDataStructureWithLongTplHeader\n";
1798
1799        assert!(yaml_output.starts_with(expected_start));
1800        // Additional checks for specific content in the YAML
1801        assert!(yaml_output.contains("device_type: HeatMeterReturn"));
1802        assert!(yaml_output.contains("identification_number:"));
1803        assert!(yaml_output.contains("status: PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3"));
1804    }
1805
1806    #[cfg(feature = "std")]
1807    #[test]
1808    fn test_json_expected_output() {
1809        use super::parse_to_json;
1810        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1811        let json_output = parse_to_json(input, None);
1812
1813        // Testing specific content in JSON
1814        assert!(json_output.contains("\"Ok\""));
1815        assert!(json_output.contains("\"LongFrame\""));
1816        assert!(json_output.contains("\"RspUd\""));
1817        assert!(json_output.contains("\"number\": 2205100"));
1818        assert!(json_output.contains("\"device_type\": \"HeatMeterReturn\""));
1819        assert!(json_output.contains("\"status\": \"PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3\""));
1820
1821        // Verify JSON structure is valid
1822        let json_parsed = serde_json::from_str::<serde_json::Value>(&json_output);
1823        assert!(json_parsed.is_ok());
1824    }
1825
1826    #[cfg(feature = "std")]
1827    #[test]
1828    fn test_mermaid_expected_output() {
1829        use super::parse_to_mermaid;
1830        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1831        let mermaid_output = parse_to_mermaid(input, None);
1832
1833        assert!(mermaid_output.starts_with("flowchart TD\n"));
1834        assert!(mermaid_output.contains("Long Frame"));
1835        assert!(mermaid_output.contains("Device Info"));
1836        assert!(mermaid_output.contains("Data Records"));
1837        assert!(mermaid_output.contains("02205100"));
1838        assert!(mermaid_output.contains("SLB"));
1839    }
1840
1841    #[cfg(feature = "std")]
1842    #[test]
1843    fn test_table_expected_output() {
1844        use super::parse_to_table;
1845        let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1846        let table_output = parse_to_table(input, None);
1847
1848        // First section of the table output
1849        assert!(table_output.starts_with("Long Frame"));
1850
1851        // Key content pieces to verify
1852        assert!(table_output.contains("RspUd (ACD: false, DFC: false)"));
1853        assert!(table_output.contains("Primary (1)"));
1854        assert!(table_output.contains("Identification Number"));
1855        assert!(table_output.contains("02205100"));
1856        assert!(table_output.contains("SLB"));
1857
1858        // Data point verifications
1859        assert!(table_output.contains("(0)e4[Wh]"));
1860        assert!(table_output.contains("(3)e-1[m³](Volume)"));
1861        assert!(table_output.contains("(1288)e-1[°C](FlowTemperature)"));
1862        assert!(table_output.contains("(12/Jan/2012)(Date)"));
1863        assert!(table_output.contains("(3383)[day]"));
1864    }
1865
1866    #[cfg(all(feature = "std", feature = "decryption"))]
1867    #[test]
1868    fn decrypted_utf8_text_is_not_rendered_as_latin1() {
1869        let input = "2E44931578563412330333637A2A00202557FB8016CA78E1243700B52E981E1918233AFE5E826DD0D4AD7854C697E7C8EB";
1870        let key = [
1871            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1872            0x0F, 0x11,
1873        ];
1874
1875        for format in ["table", "json", "yaml", "csv"] {
1876            let output = super::serialize_mbus_data(input, format, Some(&key));
1877            assert!(
1878                output.contains("m³"),
1879                "{format} output should contain the decoded UTF-8 text: {output}"
1880            );
1881            assert!(
1882                !output.contains("m³"),
1883                "{format} output contains mojibake: {output}"
1884            );
1885        }
1886    }
1887
1888    #[cfg(feature = "std")]
1889    #[test]
1890    fn test_annotated_output() {
1891        let input = "68 4D 4D 68 08 01 72 01 00 00 00 96 15 01 00 18 00 00 00 0C 78 56 00 00 00 01 FD 1B 00 02 FC 03 48 52 25 74 44 0D 22 FC 03 48 52 25 74 F1 0C 12 FC 03 48 52 25 74 63 11 02 65 B4 09 22 65 86 09 12 65 B7 09 01 72 00 72 65 00 00 B2 01 65 00 00 1F B3 16";
1892        let output = super::serialize_mbus_data(input, "annotated-legacy", None);
1893
1894        // Should be valid JSON
1895        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1896            panic!(
1897                "annotated output should be valid JSON: {}\nOutput: {}",
1898                e, output
1899            )
1900        });
1901
1902        // Should be an array
1903        assert!(parsed.is_array(), "annotated output should be a JSON array");
1904        let segments = parsed.as_array().expect("array");
1905
1906        // Should cover all 83 bytes
1907        assert!(!segments.is_empty());
1908
1909        // First segment should start at 0
1910        assert_eq!(segments[0].get("start").and_then(|v| v.as_u64()), Some(0));
1911
1912        // Last segment should end at 83
1913        let last = segments.last().expect("non-empty");
1914        assert_eq!(last.get("end").and_then(|v| v.as_u64()), Some(83));
1915
1916        // Check contiguity
1917        for window in segments.windows(2) {
1918            let end = window[0].get("end").and_then(|v| v.as_u64());
1919            let start = window[1].get("start").and_then(|v| v.as_u64());
1920            assert_eq!(end, start, "segments should be contiguous");
1921        }
1922    }
1923
1924    #[cfg(feature = "std")]
1925    #[test]
1926    fn test_hexview_output_for_ci_78_frame_is_annotated_json() {
1927        let input = "1444AE0C7856341201078C2027780B134365877AC5";
1928        let output = super::serialize_mbus_data(input, "hexview-legacy", None);
1929
1930        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1931            panic!(
1932                "hexview output should be valid annotated JSON: {}\nOutput: {}",
1933                e, output
1934            )
1935        });
1936        let segments = parsed
1937            .as_array()
1938            .expect("hexview output should be a JSON array");
1939
1940        assert!(segments.iter().any(|seg| {
1941            seg.get("kind").and_then(|v| v.as_str()) == Some("ci_field")
1942                && seg
1943                    .get("detail")
1944                    .and_then(|v| v.as_str())
1945                    .is_some_and(|detail| detail.contains("0x78"))
1946        }));
1947        assert!(segments.iter().any(|seg| {
1948            seg.get("kind").and_then(|v| v.as_str()) == Some("data_payload")
1949                && seg.get("detail").and_then(|v| v.as_str()) == Some("876543")
1950        }));
1951        assert!(!segments.iter().any(|seg| {
1952            seg.get("kind").and_then(|v| v.as_str()) == Some("unknown")
1953                || seg
1954                    .get("detail")
1955                    .and_then(|v| v.as_str())
1956                    .is_some_and(|detail| detail.contains("Unparseable data record bytes"))
1957        }));
1958    }
1959
1960    #[cfg(all(feature = "std", feature = "decryption"))]
1961    #[test]
1962    fn test_hexview_with_key_displays_decrypted_wireless_payload() {
1963        let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1964        let key = [
1965            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1966            0x0F, 0x11,
1967        ];
1968        let output = super::serialize_mbus_data(input, "hexview", Some(&key));
1969
1970        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1971            panic!(
1972                "decrypted hexview output should be valid JSON: {}\nOutput: {}",
1973                e, output
1974            )
1975        });
1976        assert_eq!(
1977            parsed.get("decrypted").and_then(|v| v.as_bool()),
1978            Some(true)
1979        );
1980
1981        let bytes = parsed
1982            .get("bytes")
1983            .and_then(|v| v.as_array())
1984            .expect("decrypted hexview should include display bytes");
1985        let display_hex = bytes
1986            .iter()
1987            .map(|byte| format!("{:02X}", byte.as_u64().unwrap_or_default()))
1988            .collect::<String>();
1989        assert!(
1990            display_hex.contains("0C1427048502046D32371F1502FD170000"),
1991            "display bytes should contain decrypted record bytes: {display_hex}"
1992        );
1993
1994        let segments = parsed
1995            .get("segments")
1996            .and_then(|v| v.as_array())
1997            .expect("decrypted hexview should include annotation segments");
1998        assert!(segments.iter().any(|seg| {
1999            seg.get("kind").and_then(|v| v.as_str()) == Some("data_payload")
2000                && seg
2001                    .get("detail")
2002                    .and_then(|v| v.as_str())
2003                    .is_some_and(|detail| detail.contains("2850427"))
2004        }));
2005        assert!(
2006            !segments
2007                .iter()
2008                .any(|seg| seg.get("kind").and_then(|v| v.as_str()) == Some("encrypted_payload")),
2009            "decrypted hexview should annotate decrypted records, not ciphertext payloads"
2010        );
2011    }
2012}