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