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