Skip to main content

m_bus_parser/
mbus_data.rs

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