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_BOX_CHARS); 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
124 if let Some(UserDataBlock::VariableDataStructure {
126 fixed_data_header,
127 variable_data_block: _,
128 }) = &parsed_data.user_data
129 {
130 let mut info_table = Table::new();
131 info_table.set_format(*format::consts::FORMAT_BOX_CHARS);
132 info_table.set_titles(row!["Field", "Value"]);
133 info_table.add_row(row![
134 "Identification Number",
135 fixed_data_header.identification_number
136 ]);
137 info_table.add_row(row![
138 "Manufacturer",
139 fixed_data_header
140 .manufacturer
141 .as_ref()
142 .map_or_else(|e| format!("Err({:?})", e), |m| format!("{:?}", m))
143 ]);
144 info_table.add_row(row!["Access Number", fixed_data_header.access_number]);
145 info_table.add_row(row!["Status", fixed_data_header.status]);
146 info_table.add_row(row!["Signature", fixed_data_header.signature]);
147 info_table.add_row(row!["Version", fixed_data_header.version]);
148 info_table.add_row(row!["Medium", fixed_data_header.medium]);
149 table_output.push_str(&info_table.to_string());
150 }
151
152 let mut value_table = Table::new();
154 value_table.set_format(*format::consts::FORMAT_BOX_CHARS);
155 value_table.set_titles(row!["Value", "Data Information", "Hex"]);
156 if let Some(data_records) = parsed_data.data_records {
157 for record in data_records.flatten() {
158 let value_information = match record
159 .data_record_header
160 .processed_data_record_header
161 .value_information
162 {
163 Some(ref x) => format!("{}", x),
164 None => "None".to_string(),
165 };
166 let data_information = match record
167 .data_record_header
168 .processed_data_record_header
169 .data_information
170 {
171 Some(ref x) => format!("{}", x),
172 None => "None".to_string(),
173 };
174 value_table.add_row(row![
175 format!("({}{})", record.data, value_information),
176 data_information,
177 record.data_hex()
178 ]);
179 }
180 }
181 table_output.push_str(&value_table.to_string());
182 }
183 frames::Frame::ShortFrame { .. } => {
184 table_output.push_str("Short Frame\n");
185 }
186 frames::Frame::SingleCharacter { .. } => {
187 table_output.push_str("Single Character Frame\n");
188 }
189 frames::Frame::ControlFrame { .. } => {
190 table_output.push_str("Control Frame\n");
191 }
192 }
193 table_output
194 } else {
195 format!("Error {:?} parsing data", parsed_data_result)
196 }
197}
198
199#[cfg(feature = "std")]
200pub fn parse_to_csv(input: &str) -> String {
201 use crate::user_data::UserDataBlock;
202 use prettytable::csv;
203
204 let data = clean_and_convert(input);
205 let parsed_data = MbusData::try_from(data.as_slice());
206
207 let mut writer = csv::Writer::from_writer(vec![]);
208
209 if let Ok(parsed_data) = parsed_data {
210 match parsed_data.frame {
211 frames::Frame::LongFrame {
212 function, address, ..
213 } => {
214 let data_point_count = parsed_data
215 .data_records
216 .as_ref()
217 .map(|records| records.clone().flatten().count())
218 .unwrap_or(0);
219
220 let mut headers = vec![
221 "FrameType".to_string(),
222 "Function".to_string(),
223 "Address".to_string(),
224 "Identification Number".to_string(),
225 "Manufacturer".to_string(),
226 "Access Number".to_string(),
227 "Status".to_string(),
228 "Signature".to_string(),
229 "Version".to_string(),
230 "Medium".to_string(),
231 ];
232
233 for i in 1..=data_point_count {
234 headers.push(format!("DataPoint{}_Value", i));
235 headers.push(format!("DataPoint{}_Info", i));
236 }
237
238 let header_refs: Vec<&str> = headers.iter().map(|s| s.as_str()).collect();
239 writer
240 .write_record(header_refs)
241 .map_err(|_| ())
242 .unwrap_or_default();
243
244 let mut row = vec![
245 "LongFrame".to_string(),
246 function.to_string(),
247 address.to_string(),
248 ];
249
250 match &parsed_data.user_data {
251 Some(UserDataBlock::VariableDataStructure {
252 fixed_data_header, ..
253 }) => {
254 row.extend_from_slice(&[
255 fixed_data_header.identification_number.to_string(),
256 fixed_data_header
257 .manufacturer
258 .as_ref()
259 .map_or_else(|e| format!("Err({:?})", e), |m| format!("{:?}", m)),
260 fixed_data_header.access_number.to_string(),
261 fixed_data_header.status.to_string(),
262 fixed_data_header.signature.to_string(),
263 fixed_data_header.version.to_string(),
264 fixed_data_header.medium.to_string(),
265 ]);
266 }
267 Some(UserDataBlock::FixedDataStructure {
268 identification_number,
269 access_number,
270 status,
271 ..
272 }) => {
273 row.extend_from_slice(&[
274 identification_number.to_string(),
275 "".to_string(), access_number.to_string(),
277 status.to_string(),
278 "".to_string(), "".to_string(), "".to_string(), ]);
282 }
283 _ => {
284 for _ in 0..7 {
286 row.push("".to_string());
287 }
288 }
289 }
290
291 if let Some(data_records) = parsed_data.data_records {
292 for record in data_records.flatten() {
293 let parsed_value = format!("{}", record.data);
295
296 let value_information = match record
298 .data_record_header
299 .processed_data_record_header
300 .value_information
301 {
302 Some(x) => format!("{}", x),
303 None => "None".to_string(),
304 };
305
306 let formatted_value = format!("({}){}", parsed_value, value_information);
308
309 let data_information = match record
310 .data_record_header
311 .processed_data_record_header
312 .data_information
313 {
314 Some(x) => format!("{}", x),
315 None => "None".to_string(),
316 };
317
318 row.push(formatted_value);
319 row.push(data_information);
320 }
321 }
322
323 let row_refs: Vec<&str> = row.iter().map(|s| s.as_str()).collect();
324 writer
325 .write_record(row_refs)
326 .map_err(|_| ())
327 .unwrap_or_default();
328 }
329 _ => {
330 writer
331 .write_record(["FrameType"])
332 .map_err(|_| ())
333 .unwrap_or_default();
334 writer
335 .write_record([format!("{:?}", parsed_data.frame).as_str()])
336 .map_err(|_| ())
337 .unwrap_or_default();
338 }
339 }
340 } else {
341 writer
342 .write_record(["Error"])
343 .map_err(|_| ())
344 .unwrap_or_default();
345 writer
346 .write_record(["Error parsing data"])
347 .map_err(|_| ())
348 .unwrap_or_default();
349 }
350
351 let csv_data = writer.into_inner().unwrap_or_default();
352 String::from_utf8(csv_data)
353 .unwrap_or_else(|_| "Error converting CSV data to string".to_string())
354}
355
356#[cfg(test)]
357mod tests {
358
359 #[cfg(feature = "std")]
360 #[test]
361 fn test_csv_converter() {
362 use super::parse_to_csv;
363 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";
364 let csv_output: String = parse_to_csv(input);
365 println!("{}", csv_output);
366 let yaml_output: String = super::parse_to_yaml(input);
367 println!("{}", yaml_output);
368 let json_output: String = super::parse_to_json(input);
369 println!("{}", json_output);
370 let table_output: String = super::parse_to_table(input);
371 println!("{}", table_output);
372 }
373
374 #[cfg(feature = "std")]
375 #[test]
376 fn test_csv_expected_output() {
377 use super::parse_to_csv;
378 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";
379 let csv_output = parse_to_csv(input);
380
381 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";
382
383 assert_eq!(csv_output, expected);
384 }
385
386 #[cfg(feature = "std")]
387 #[test]
388 fn test_yaml_expected_output() {
389 use super::parse_to_yaml;
390 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";
391 let yaml_output = parse_to_yaml(input);
392
393 let expected_start = "!Ok\nframe: !LongFrame\n function: !RspUd\n acd: false\n dfc: false\n address: !Primary 1\nuser_data: !VariableDataStructure\n";
395
396 assert!(yaml_output.starts_with(expected_start));
397 assert!(yaml_output.contains("medium: Heat"));
399 assert!(yaml_output.contains("identification_number:"));
400 assert!(yaml_output.contains("status: PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3"));
401 }
402
403 #[cfg(feature = "std")]
404 #[test]
405 fn test_json_expected_output() {
406 use super::parse_to_json;
407 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";
408 let json_output = parse_to_json(input);
409
410 assert!(json_output.contains("\"Ok\""));
412 assert!(json_output.contains("\"LongFrame\""));
413 assert!(json_output.contains("\"RspUd\""));
414 assert!(json_output.contains("\"number\": 2205100"));
415 assert!(json_output.contains("\"medium\": \"Heat\""));
416 assert!(json_output.contains("\"status\": \"PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3\""));
417
418 let json_parsed = serde_json::from_str::<serde_json::Value>(&json_output);
420 assert!(json_parsed.is_ok());
421 }
422
423 #[cfg(feature = "std")]
424 #[test]
425 fn test_table_expected_output() {
426 use super::parse_to_table;
427 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";
428 let table_output = parse_to_table(input);
429
430 assert!(table_output.starts_with("Long Frame"));
432
433 assert!(table_output.contains("RspUd (ACD: false, DFC: false)"));
435 assert!(table_output.contains("Primary (1)"));
436 assert!(table_output.contains("Identification Number"));
437 assert!(table_output.contains("02205100"));
438 assert!(table_output.contains("ManufacturerCode { code: ['S', 'L', 'B'] }"));
439
440 assert!(table_output.contains("(0)e4[Wh]"));
442 assert!(table_output.contains("(3)e-1[m³](Volume)"));
443 assert!(table_output.contains("(1288)e-1[°C]"));
444 assert!(table_output.contains("(12/Jan/12)(Date)"));
445 assert!(table_output.contains("(3383)[day]"));
446 }
447}