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