1#[cfg(feature = "std")]
2use prettytable::{format, row, Table};
3
4use crate::frames;
5use crate::user_data;
6use crate::MbusError;
7
8#[cfg_attr(
9 feature = "serde",
10 derive(serde::Serialize),
11 serde(bound(deserialize = "'de: 'a"))
12)]
13#[derive(Debug)]
14pub struct MbusData<'a> {
15 pub frame: frames::Frame<'a>,
16 pub user_data: Option<user_data::UserDataBlock<'a>>,
17 pub data_records: Option<user_data::DataRecords<'a>>,
18}
19
20impl<'a> TryFrom<&'a [u8]> for MbusData<'a> {
21 type Error = MbusError;
22
23 fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
24 let frame = frames::Frame::try_from(data)?;
25 let mut user_data = None;
26 let mut data_records = None;
27 match &frame {
28 frames::Frame::LongFrame { data, .. } => {
29 if let Ok(x) = user_data::UserDataBlock::try_from(*data) {
30 user_data = Some(x);
31 if let Ok(user_data::UserDataBlock::VariableDataStructure {
32 fixed_data_header: _,
33 variable_data_block,
34 }) = user_data::UserDataBlock::try_from(*data)
35 {
36 data_records = Some(variable_data_block.into());
37 }
38 }
39 }
40 frames::Frame::SingleCharacter { .. } => (),
41 frames::Frame::ShortFrame { .. } => (),
42 frames::Frame::ControlFrame { .. } => (),
43 };
44
45 Ok(MbusData {
46 frame,
47 user_data,
48 data_records,
49 })
50 }
51}
52
53#[cfg(feature = "std")]
54fn clean_and_convert(input: &str) -> Vec<u8> {
55 use core::str;
56 let input = input.trim();
57 let cleaned_data: String = input.replace("0x", "").replace([' ', ',', 'x'], "");
58
59 cleaned_data
60 .as_bytes()
61 .chunks(2)
62 .map(|chunk| {
63 let byte_str = str::from_utf8(chunk).unwrap_or_default();
64 u8::from_str_radix(byte_str, 16).unwrap_or_default()
65 })
66 .collect()
67}
68
69#[cfg(feature = "std")]
70pub fn serialize_mbus_data(data: &str, format: &str) -> String {
71 match format {
72 "json" => parse_to_json(data),
73 "yaml" => parse_to_yaml(data),
74 "csv" => parse_to_csv(data).to_string(),
75 _ => parse_to_table(data).to_string(),
76 }
77}
78
79#[cfg(feature = "std")]
80pub fn parse_to_json(input: &str) -> String {
81 let data = clean_and_convert(input);
82 let parsed_data = MbusData::try_from(data.as_slice());
83
84 serde_json::to_string_pretty(&parsed_data)
85 .unwrap_or_default()
86 .to_string()
87}
88
89#[cfg(feature = "std")]
90fn parse_to_yaml(input: &str) -> String {
91 let data = clean_and_convert(input);
92 let parsed_data = MbusData::try_from(data.as_slice());
93
94 serde_yaml::to_string(&parsed_data)
95 .unwrap_or_default()
96 .to_string()
97}
98
99#[cfg(feature = "std")]
100fn parse_to_table(input: &str) -> String {
101 use user_data::UserDataBlock;
102
103 let data = clean_and_convert(input);
104
105 let mut table_output = String::new();
106 let parsed_data_result = MbusData::try_from(data.as_slice());
107 if let Ok(parsed_data) = parsed_data_result {
108 let mut table = Table::new();
109 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
110
111 match parsed_data.frame {
112 frames::Frame::LongFrame {
113 function,
114 address,
115 data: _,
116 } => {
117 table_output.push_str("Long Frame \n");
118
119 table.set_titles(row!["Function", "Address"]);
120 table.add_row(row![function, address]);
121
122 table_output.push_str(&table.to_string());
123 table = Table::new();
124
125 match parsed_data.user_data {
126 Some(UserDataBlock::VariableDataStructure {
127 fixed_data_header,
128 variable_data_block: _,
129 }) => {
130 let mut info_table = Table::new();
132 info_table.set_format(*format::consts::FORMAT_BOX_CHARS);
133 info_table.set_titles(row!["Field", "Value"]);
134 info_table.add_row(row![
135 "Identification Number",
136 fixed_data_header.identification_number
137 ]);
138 info_table.add_row(row![
139 "Manufacturer",
140 fixed_data_header
141 .manufacturer
142 .as_ref()
143 .map_or_else(|e| format!("Err({:?})", e), |m| format!("{:?}", m))
144 ]);
145 info_table.add_row(row!["Access Number", fixed_data_header.access_number]);
146 info_table.add_row(row!["Status", fixed_data_header.status]);
147 info_table.add_row(row!["Signature", fixed_data_header.signature]);
148 info_table.add_row(row!["Version", fixed_data_header.version]);
149 info_table.add_row(row!["Medium", fixed_data_header.medium]);
150 table_output.push_str(&info_table.to_string());
151 }
152 Some(UserDataBlock::FixedDataStructure {
153 identification_number,
154 access_number,
155 status,
156 medium_ad_unit,
157 counter1,
158 counter2,
159 }) => {
160 table.set_titles(row![
161 "Identification Number",
162 "Access Number",
163 "Status",
164 "Medium Ad Unit",
165 "Counter 1",
166 "Counter 2",
167 ]);
168 table.add_row(row![
169 identification_number,
170 access_number,
171 status,
172 medium_ad_unit,
173 counter1,
174 counter2,
175 ]);
176 }
177 Some(UserDataBlock::ResetAtApplicationLevel { subcode }) => {
178 table.set_titles(row!["Function", "Address", "Subcode"]);
179 table.add_row(row![function, address, subcode]);
180 }
181 None => {
182 table.set_titles(row!["Function", "Address"]);
183 table.add_row(row![function, address]);
184 }
185 }
186
187 table_output.push_str(&table.to_string());
188 table = Table::new();
189
190 table.set_titles(row!["Value", "Data Information",]);
191
192 if let Some(data_records) = parsed_data.data_records {
193 for record in data_records.flatten() {
194 let value_information = match record
195 .data_record_header
196 .processed_data_record_header
197 .value_information
198 {
199 Some(x) => format!("{}", x),
200 None => "None".to_string(),
201 };
202
203 let data_information = match record
204 .data_record_header
205 .processed_data_record_header
206 .data_information
207 {
208 Some(x) => format!("{}", x),
209 None => "None".to_string(),
210 };
211
212 table.add_row(row![
213 format!("({}{}", record.data, value_information),
214 format!("{}", data_information)
215 ]);
216 }
217 }
218 }
219 frames::Frame::ShortFrame { .. } => {
220 table_output.push_str("Short Frame\n");
221 }
222 frames::Frame::SingleCharacter { .. } => {
223 table_output.push_str("Single Character Frame\n");
224 }
225 frames::Frame::ControlFrame { .. } => {
226 table_output.push_str("Control Frame\n");
227 }
228 }
229
230 table_output.push_str(&table.to_string());
231 table_output
232 } else {
233 format!("Error {:?} parsing data", parsed_data_result)
234 }
235}
236
237#[cfg(feature = "std")]
238pub fn parse_to_csv(input: &str) -> String {
239 use crate::user_data::UserDataBlock;
240 use prettytable::csv;
241
242 let data = clean_and_convert(input);
243 let parsed_data = MbusData::try_from(data.as_slice());
244
245 let mut writer = csv::Writer::from_writer(vec![]);
246
247 if let Ok(parsed_data) = parsed_data {
248 match parsed_data.frame {
249 frames::Frame::LongFrame {
250 function, address, ..
251 } => {
252 let data_point_count = parsed_data
253 .data_records
254 .as_ref()
255 .map(|records| records.clone().flatten().count())
256 .unwrap_or(0);
257
258 let mut headers = vec![
259 "FrameType".to_string(),
260 "Function".to_string(),
261 "Address".to_string(),
262 "Identification Number".to_string(),
263 "Manufacturer".to_string(),
264 "Access Number".to_string(),
265 "Status".to_string(),
266 "Signature".to_string(),
267 "Version".to_string(),
268 "Medium".to_string(),
269 ];
270
271 for i in 1..=data_point_count {
272 headers.push(format!("DataPoint{}_Value", i));
273 headers.push(format!("DataPoint{}_Info", i));
274 }
275
276 let header_refs: Vec<&str> = headers.iter().map(|s| s.as_str()).collect();
277 writer
278 .write_record(header_refs)
279 .map_err(|_| ())
280 .unwrap_or_default();
281
282 let mut row = vec![
283 "LongFrame".to_string(),
284 function.to_string(),
285 address.to_string(),
286 ];
287
288 match &parsed_data.user_data {
289 Some(UserDataBlock::VariableDataStructure {
290 fixed_data_header, ..
291 }) => {
292 row.extend_from_slice(&[
293 fixed_data_header.identification_number.to_string(),
294 fixed_data_header
295 .manufacturer
296 .as_ref()
297 .map_or_else(|e| format!("Err({:?})", e), |m| format!("{:?}", m)),
298 fixed_data_header.access_number.to_string(),
299 fixed_data_header.status.to_string(),
300 fixed_data_header.signature.to_string(),
301 fixed_data_header.version.to_string(),
302 fixed_data_header.medium.to_string(),
303 ]);
304 }
305 Some(UserDataBlock::FixedDataStructure {
306 identification_number,
307 access_number,
308 status,
309 ..
310 }) => {
311 row.extend_from_slice(&[
312 identification_number.to_string(),
313 "".to_string(), access_number.to_string(),
315 status.to_string(),
316 "".to_string(), "".to_string(), "".to_string(), ]);
320 }
321 _ => {
322 for _ in 0..7 {
324 row.push("".to_string());
325 }
326 }
327 }
328
329 if let Some(data_records) = parsed_data.data_records {
330 for record in data_records.flatten() {
331 let parsed_value = format!("{}", record.data);
333
334 let value_information = match record
336 .data_record_header
337 .processed_data_record_header
338 .value_information
339 {
340 Some(x) => format!("{}", x),
341 None => "None".to_string(),
342 };
343
344 let formatted_value = format!("({}){}", parsed_value, value_information);
346
347 let data_information = match record
348 .data_record_header
349 .processed_data_record_header
350 .data_information
351 {
352 Some(x) => format!("{}", x),
353 None => "None".to_string(),
354 };
355
356 row.push(formatted_value);
357 row.push(data_information);
358 }
359 }
360
361 let row_refs: Vec<&str> = row.iter().map(|s| s.as_str()).collect();
362 writer
363 .write_record(row_refs)
364 .map_err(|_| ())
365 .unwrap_or_default();
366 }
367 _ => {
368 writer
369 .write_record(["FrameType"])
370 .map_err(|_| ())
371 .unwrap_or_default();
372 writer
373 .write_record([format!("{:?}", parsed_data.frame).as_str()])
374 .map_err(|_| ())
375 .unwrap_or_default();
376 }
377 }
378 } else {
379 writer
380 .write_record(["Error"])
381 .map_err(|_| ())
382 .unwrap_or_default();
383 writer
384 .write_record(["Error parsing data"])
385 .map_err(|_| ())
386 .unwrap_or_default();
387 }
388
389 let csv_data = writer.into_inner().unwrap_or_default();
390 String::from_utf8(csv_data)
391 .unwrap_or_else(|_| "Error converting CSV data to string".to_string())
392}
393
394#[cfg(test)]
395mod tests {
396
397 #[cfg(feature = "std")]
398 #[test]
399 fn test_csv_converter() {
400 use super::parse_to_csv;
401 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";
402 let csv_output: String = parse_to_csv(input);
403 println!("{}", csv_output);
404 let yaml_output: String = super::parse_to_yaml(input);
405 println!("{}", yaml_output);
406 let json_output: String = super::parse_to_json(input);
407 println!("{}", json_output);
408 let table_output: String = super::parse_to_table(input);
409 println!("{}", table_output);
410 }
411
412 #[cfg(feature = "std")]
413 #[test]
414 fn test_csv_expected_output() {
415 use super::parse_to_csv;
416 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";
417 let csv_output = parse_to_csv(input);
418
419 let expected = "FrameType,Function,Address,Identification Number,Manufacturer,Access Number,Status,Signature,Version,Medium,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,\"ManufacturerCode { code: ['S', 'L', 'B'] }\",0,\"Permanent error, Manufacturer specific 3\",0,2,Heat,(0))e4[Wh],\"0,Inst,32-bit Integer\",(3))e-1[m³](Volume),\"0,Inst,BCD 8-digit\",(0))e3[W],\"0,Inst,BCD 6-digit\",(0))e-3[m³h⁻¹],\"0,Inst,BCD 6-digit\",(1288))e-1[°C],\"0,Inst,BCD 4-digit\",(516))e-1[°C],\"0,Inst,BCD 4-digit\",(7723))e-2[°K],\"0,Inst,BCD 6-digit\",(12/Jan/12))(Date),\"0,Inst,Date Type G\",(3383))[day],\"0,Inst,16-bit Integer\",\"(Manufacturer Specific: [15, 96, 0])None\",None\n";
420
421 assert_eq!(csv_output, expected);
422 }
423
424 #[cfg(feature = "std")]
425 #[test]
426 fn test_yaml_expected_output() {
427 use super::parse_to_yaml;
428 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";
429 let yaml_output = parse_to_yaml(input);
430
431 let expected_start = "!Ok\nframe: !LongFrame\n function: !RspUd\n acd: false\n dfc: false\n address: !Primary 1\nuser_data: !VariableDataStructure\n";
433
434 assert!(yaml_output.starts_with(expected_start));
435 assert!(yaml_output.contains("medium: Heat"));
437 assert!(yaml_output.contains("identification_number:"));
438 assert!(yaml_output.contains("status: PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3"));
439 }
440
441 #[cfg(feature = "std")]
442 #[test]
443 fn test_json_expected_output() {
444 use super::parse_to_json;
445 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";
446 let json_output = parse_to_json(input);
447
448 assert!(json_output.contains("\"Ok\""));
450 assert!(json_output.contains("\"LongFrame\""));
451 assert!(json_output.contains("\"RspUd\""));
452 assert!(json_output.contains("\"number\": 2205100"));
453 assert!(json_output.contains("\"medium\": \"Heat\""));
454 assert!(json_output.contains("\"status\": \"PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3\""));
455
456 let json_parsed = serde_json::from_str::<serde_json::Value>(&json_output);
458 assert!(json_parsed.is_ok());
459 }
460
461 #[cfg(feature = "std")]
462 #[test]
463 fn test_table_expected_output() {
464 use super::parse_to_table;
465 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";
466 let table_output = parse_to_table(input);
467
468 assert!(table_output.starts_with("Long Frame"));
470
471 assert!(table_output.contains("RspUd (ACD: false, DFC: false)"));
473 assert!(table_output.contains("Primary (1)"));
474 assert!(table_output.contains("Identification Number"));
475 assert!(table_output.contains("02205100"));
476 assert!(table_output.contains("ManufacturerCode { code: ['S', 'L', 'B'] }"));
477
478 assert!(table_output.contains("(0)e4[Wh]"));
480 assert!(table_output.contains("(3)e-1[m³](Volume)"));
481 assert!(table_output.contains("(1288)e-1[°C]"));
482 assert!(table_output.contains("(12/Jan/12)(Date)"));
483 assert!(table_output.contains("(3383)[day]"));
484 }
485}