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_BOX_CHARS); // Use box chars for top table
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
124                // Info table (box style, no extra newlines)
125                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                // Value/Data Information table (all lines the same, no extra newlines)
153                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(), // Manufacturer
276                            access_number.to_string(),
277                            status.to_string(),
278                            "".to_string(), // Signature
279                            "".to_string(), // Version
280                            "".to_string(), // Medium
281                        ]);
282                    }
283                    _ => {
284                        // Fill with empty strings for header info
285                        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                        // Format the value with units to match the table output
294                        let parsed_value = format!("{}", record.data);
295
296                        // Get value information including units
297                        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                        // Format the value similar to the table output with units
307                        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        // First line of YAML output to test against - we'll test just the beginning to avoid a massive string
394        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        // Additional checks for specific content in the YAML
398        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        // Testing specific content in JSON
411        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        // Verify JSON structure is valid
419        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        // First section of the table output
431        assert!(table_output.starts_with("Long Frame"));
432
433        // Key content pieces to verify
434        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        // Data point verifications
441        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}