m_bus_parser/
mbus_data.rs

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                        // Key-value table for all fields
131                        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(), // Manufacturer
314                            access_number.to_string(),
315                            status.to_string(),
316                            "".to_string(), // Signature
317                            "".to_string(), // Version
318                            "".to_string(), // Medium
319                        ]);
320                    }
321                    _ => {
322                        // Fill with empty strings for header info
323                        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                        // Format the value with units to match the table output
332                        let parsed_value = format!("{}", record.data);
333
334                        // Get value information including units
335                        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                        // Format the value similar to the table output with units
345                        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        // First line of YAML output to test against - we'll test just the beginning to avoid a massive string
432        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        // Additional checks for specific content in the YAML
436        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        // Testing specific content in JSON
449        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        // Verify JSON structure is valid
457        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        // First section of the table output
469        assert!(table_output.starts_with("Long Frame"));
470
471        // Key content pieces to verify
472        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        // Data point verifications
479        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}