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