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