Skip to main content

m_bus_parser/
mbus_data.rs

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