Skip to content

Commit 9723d97

Browse files
authored
Merge pull request #71 from maebli/feature/wmbus
Feature/wmbus
2 parents 0e29baa + 5d6abb9 commit 9723d97

42 files changed

Lines changed: 4728 additions & 1449 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
11+
## [0.1.0]
12+
13+
- wmbus parsing capabilities
14+
- preperation for decryption
15+
- breaking changes to API for using the lib
16+
- refacatoring things into core to be shared by wireless and wired parsing parts

Cargo.toml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "m-bus-parser"
3-
version = "0.0.28"
3+
version = "0.1.0"
44
edition = "2021"
55
description = "A library for parsing M-Bus frames"
66
license = "MIT"
@@ -18,16 +18,18 @@ hex = "0.4"
1818
serde = "1.0.217"
1919
serde_derive = "1.0.217"
2020
serde-xml-rs = "0.7.0"
21+
serde_json = "1.0"
2122

2223
[build-dependencies]
2324
bindgen = "0.72.0"
2425

2526
[features]
2627
default = []
27-
std = ["prettytable-rs", "serde_json", "serde_yaml", "serde"]
28-
plaintext-before-extension = []
29-
serde = ["dep:serde", "arrayvec/serde", "bitflags/serde"]
30-
defmt = ["dep:defmt"]
28+
std = ["prettytable-rs", "serde_json", "serde_yaml", "serde", "wired-mbus-link-layer/std", "wireless-mbus-link-layer/std", "m-bus-core/std", "m-bus-application-layer/std"]
29+
plaintext-before-extension = ["m-bus-application-layer/plaintext-before-extension"]
30+
serde = ["dep:serde", "arrayvec/serde", "bitflags/serde", "wired-mbus-link-layer/serde", "wireless-mbus-link-layer/serde", "m-bus-core/serde", "m-bus-application-layer/serde"]
31+
defmt = ["dep:defmt", "wired-mbus-link-layer/defmt", "wireless-mbus-link-layer/defmt", "m-bus-core/defmt", "m-bus-application-layer/defmt"]
32+
decryption = ["dep:aes", "dep:cbc", "dep:cipher", "dep:aes-gcm", "dep:ccm", "m-bus-application-layer/decryption"]
3133

3234
[profile.release]
3335
opt-level = 'z' # Optimize for size
@@ -45,8 +47,18 @@ serde = { version = "1.0", features = ["derive"], optional = true }
4547
bitflags = "2.8.0"
4648
arrayvec = { version = "0.7.4", default-features = false }
4749
defmt = { version = "1.0.1", optional = true }
50+
wired-mbus-link-layer = {path = "crates/wired-mbus-link-layer"}
51+
wireless-mbus-link-layer = {path = "crates/wireless-mbus-link-layer"}
52+
m-bus-core = {path = "crates/m-bus-core"}
53+
m-bus-application-layer = {path = "crates/m-bus-application-layer"}
54+
aes = { version = "0.8", optional = true, default-features = false }
55+
cbc = { version = "0.1", optional = true, default-features = false }
56+
cipher = { version = "0.4", optional = true, default-features = false, features = ["block-padding"] }
57+
aes-gcm = { version = "0.10", optional = true, default-features = false, features = ["aes"] }
58+
ccm = { version = "0.5", optional = true, default-features = false }
59+
4860
[workspace]
49-
members = ["cli", "wasm","python"]
61+
members = ["cli", "wasm","python", "crates/m-bus-application-layer", "crates/wired-mbus-link-layer", "crates/wireless-mbus-link-layer", "crates/m-bus-core"]
5062
exclude = ["examples/cortex-m"]
5163

5264
[[bench]]

README.md

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
1-
2-
# m-bus-parser (wired)
1+
# m-bus-parser
32

43
[![Discord](https://img.shields.io/badge/Discord-Join%20Now-blue?style=flat&logo=Discord)](https://discord.gg/FfmecQ4wua)
54
[![Crates.io](https://img.shields.io/crates/v/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![Downloads](https://img.shields.io/crates/d/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![License](https://img.shields.io/crates/l/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![Documentation](https://docs.rs/m-bus-parser/badge.svg)](https://docs.rs/m-bus-parser) [![Build Status](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml/badge.svg)](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml)
65

76

87
### Introduction
98

10-
*For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md)*
9+
*For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md), for change history see [CHANGELOG.md](./CHANGELOG.md),*
1110

12-
m-bus-parser is an open source parser (sometimes also refered to as decoder and/or deserializer) of **wired** m-bus portocol and is written in rust.
11+
m-bus-parser is an open source parser (sometimes also refered to as decoder and/or deserializer) of **wired** and **wireless** m-bus portocol and is written in rust.
1312

1413
"M-Bus or Meter-Bus is a European standard (EN 13757-2 physical and link layer, EN 13757-3 application layer) for the remote reading of water, gas or electricity meters. M-Bus is also usable for other types of consumption meters, such as heating systems or water meters. The M-Bus interface is made for communication on two wires, making it cost-effective." - [Wikipedia](https://en.wikipedia.org/wiki/Meter-Bus)
1514

1615
An outdated specification is available freely on the [m-bus website](https://m-bus.com/documentation). This document is a good starting point for understanding the protocol. There have been many other implementations of the specification.
1716

1817
Furthermore, the Open Metering System (OMS) Group has published a specification for the m-bus protocol. This specification is available for free on the [OMS website](https://www.oms-group.org/en/) or more specificially [here](https://oms-group.org/en/open-metering-system/oms-specification).
1918

19+
There are many m bus parsers in the wild on github, such as a no longer maitained [ m-bus encoder and decoder by rscada](https://github.com/rscada/libmbus) written in **c**, [jMbus](https://github.com/qvest-digital/jmbus) written in **java**,[Valley.Net.Protocols.MeterBus](https://github.com/sympthom/Valley.Net.Protocols.MeterBus/) written in **C#**, [tmbus](https://dev-lab.github.io/tmbus/) written in javascript or [pyMeterBus](https://github.com/ganehag/pyMeterBus) written in python.
20+
21+
## Supported Features
22+
23+
### Control Information Types
24+
25+
The parser currently supports the following Control Information (CI) types:
26+
27+
#### Implemented
28+
- **ResetAtApplicationLevel** - Application layer reset
29+
- **ResponseWithVariableDataStructure** - Variable data response (CI: 0x72, 0x76, 0x7A)
30+
- **ResponseWithFixedDataStructure** - Fixed data response (CI: 0x73)
31+
- **ApplicationLayerShortTransport** - Short transport layer frame (CI: 0x7D)
32+
- **ApplicationLayerLongTransport** - Long transport layer frame (CI: 0x7E)
33+
- **ExtendedLinkLayerI** - Extended link layer type I (CI: 0x8A)
34+
35+
#### Not Yet Implemented
36+
The following CI types will return an `ApplicationLayerError::Unimplemented` error:
37+
- SendData, SelectSlave, SynchronizeSlave
38+
- SetBaudRate* (300, 600, 1200, 2400, 4800, 9600, 19200, 38400)
39+
- OutputRAMContent, WriteRAMContent
40+
- StartCalibrationTestMode, ReadEEPROM, StartSoftwareTest
41+
- HashProcedure, SendErrorStatus, SendAlarmStatus
42+
- DataSentWith*TransportLayer, CosemData*, ObisData*
43+
- ApplicationLayerFormatFrame*, ClockSync*
44+
- ApplicationError*, Alarm*, NetworkLayer*
45+
- TransportLayer* (various types)
46+
- ExtendedLinkLayerII, ExtendedLinkLayerIII
47+
48+
For a complete list, refer to EN 13757-3 specification.
49+
50+
### Value Information Units
2051

21-
such as a no longer maitained [ m-bus encoder and decoder by rscada](https://github.com/rscada/libmbus) written in **c**, [jMbus](https://github.com/qvest-digital/jmbus) written in **java**,[Valley.Net.Protocols.MeterBus](https://github.com/sympthom/Valley.Net.Protocols.MeterBus/) written in **C#**, [tmbus](https://dev-lab.github.io/tmbus/) written in javascript or [pyMeterBus](https://github.com/ganehag/pyMeterBus) written in python.
52+
Most common value information unit codes are supported. Some specialized units may return `DataInformationError::Unimplemented`:
53+
- Reserved length values in variable length data
54+
- Special functions data parsing
55+
- Partial primary and extended value information unit codes
2256

57+
Contributions to implement additional CI types and value information units are welcome!
2358

2459
## Dependants and Deployments
2560

@@ -43,7 +78,13 @@ The are some python bindings, the source is in the sub folder "python" and is pu
4378

4479
### Visualization of Library Function
4580

46-
Do not get confused about the different types of frame types. The most important one to understand at first is the `LongFrame` which is the most common frame type. The others are for example for searching for a slave or for setting the primary address of a slave. This is not of primary intrest for most users. Visualization was made with the help of the tool [excalidraw](https://excalidraw.com/).
81+
## Wireless Link Layer
82+
83+
![](./resources/wireless-frame.png)
84+
85+
## Wired Link Layer
86+
87+
The most common wired frame is the `LongFrame`.
4788

4889
![](./resources/function.png)
4990

@@ -58,17 +99,8 @@ The searlized application layer above can be further broken into parsable parts.
5899

59100
![](./resources/application-layer-valueinformationblock.png)
60101

61-
## Aim
62-
63-
- suitable for embedded targets `no_std`
64-
- Follow the Rust API Guideline https://rust-lang.github.io/api-guidelines/
65-
- minimal copy
66102

67-
## Development status
68-
69-
The library is currently under development. It is able to parse the link layer but not the application layer. The next goal is to parse the application layer. Once this is achieved the library will be released as `v0.1.0`. Further goals, such as decryption, will be set after this milestone is achieved.
70-
71-
## Example of current function
103+
## Simple example, parsing wired m bus frame
72104

73105
Examples taken from https://m-bus.com/documentation-wired/06-application-layer:
74106

@@ -79,19 +111,19 @@ Examples taken from https://m-bus.com/documentation-wired/06-application-layer:
79111
Parsing the frame using the library (the data is not yet parsable with the lib):
80112

81113
```rust
82-
83-
use m_bus_parser::frames::{Address, Frame, Function};
84114

85-
let example = vec![
86-
0x68, 0x06, 0x06, 0x68,
87-
0x53, 0xFE, 0x51,
88-
0x01, 0x7A, 0x08,
115+
use m_bus_parser::{Address, WiredFrame, Function};
116+
117+
let example = vec![
118+
0x68, 0x06, 0x06, 0x68,
119+
0x53, 0xFE, 0x51,
120+
0x01, 0x7A, 0x08,
89121
0x25, 0x16,
90122
];
91123

92-
let frame = Frame::try_from(example.as_slice()))?;
124+
let frame = WiredFrame::try_from(example.as_slice())?;
93125

94-
if let Frame::ControlFrame { function, address, data } = frame {
126+
if let WiredFrame::ControlFrame { function, address, data } = frame {
95127
assert_eq!(address, Address::Broadcast { reply_required: true });
96128
assert_eq!(function, Function::SndUd { fcb: (false)});
97129
assert_eq!(data, &[0x51,0x01, 0x7A, 0x08]);

benches/bench.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2-
use m_bus_parser::frames::Frame;
1+
use criterion::{criterion_group, criterion_main, Criterion};
2+
use m_bus_parser::mbus_data::MbusData;
3+
use m_bus_parser::WiredFrame;
4+
use std::hint::black_box;
35

6+
#[allow(clippy::unwrap_used)]
47
fn frame_parse_benchmark(c: &mut Criterion) {
5-
let data = vec![0x68, 0x04, 0x04, 0x68, 0x53, 0x01, 0x00, 0x00, 0x54, 0x16];
8+
let data: Vec<u8> = vec![0x68, 0x04, 0x04, 0x68, 0x53, 0x01, 0x00, 0x00, 0x54, 0x16];
69
c.bench_function("parse_frame_only", |b| {
710
b.iter(|| {
811
// Use black_box to prevent compiler optimizations from skipping the computation
9-
Frame::try_from(black_box(data.as_slice())).unwrap();
12+
WiredFrame::try_from(black_box(data.as_slice())).unwrap();
1013
})
1114
});
1215
}
1316

17+
#[allow(clippy::unwrap_used)]
1418
fn m_bus_parser_benchmark(c: &mut Criterion) {
15-
let data = vec![
19+
let data: Vec<u8> = vec![
1620
0x68, 0x3C, 0x3C, 0x68, 0x08, 0x08, 0x72, 0x78, 0x03, 0x49, 0x11, 0x77, 0x04, 0x0E, 0x16,
1721
0x0A, 0x00, 0x00, 0x00, 0x0C, 0x78, 0x78, 0x03, 0x49, 0x11, 0x04, 0x13, 0x31, 0xD4, 0x00,
1822
0x00, 0x42, 0x6C, 0x00, 0x00, 0x44, 0x13, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6D, 0x0B, 0x0B,
@@ -21,7 +25,7 @@ fn m_bus_parser_benchmark(c: &mut Criterion) {
2125
];
2226
c.bench_function("parse", |b| {
2327
b.iter(|| {
24-
m_bus_parser::MbusData::try_from(data.as_slice()).unwrap();
28+
MbusData::<WiredFrame>::try_from(data.as_slice()).unwrap();
2529
})
2630
});
2731
}

cli/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "m-bus-parser-cli"
3-
version = "0.0.16"
3+
version = "0.1.0"
44
edition = "2021"
55
description = "A cli to use the library for parsing M-Bus frames"
66
license = "MIT"
@@ -18,8 +18,10 @@ tag-name = "cli-v{{version}}"
1818
[build-dependencies]
1919

2020
[features]
21-
21+
default = ["decryption"]
22+
decryption = ["m-bus-parser/decryption"]
2223

2324
[dependencies]
24-
m-bus-parser = { path = "..", version = "0.0.28", features = ["std", "serde"] }
25+
m-bus-parser = { path = "..", version = "0.1.0", features = ["std", "serde"] }
26+
hex = "0.4"
2527
clap = { version = "4.5.4", features = ["derive"] }

cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Long Frame
6161
├───────────────────────┼────────────────────────────────────────────┤
6262
│ Version │ 2 │
6363
├───────────────────────┼────────────────────────────────────────────┤
64-
Medium │ Heat
64+
Device Type │ Heat Meter
6565
└───────────────────────┴────────────────────────────────────────────┘
6666
┌──────────────────────────────────────────┬───────────────────────┬─────────────┐
6767
│ Value │ Data Information │ Hex │

cli/src/main.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,70 @@ enum Command {
2525

2626
#[arg(short = 't', long)]
2727
format: Option<String>,
28+
29+
/// Decryption key (32 hex characters for AES-128)
30+
#[arg(short = 'k', long)]
31+
key: Option<String>,
2832
},
2933
}
3034

3135
fn main() {
3236
let cli = Cli::parse();
3337

3438
match cli.command {
35-
Command::Parse { file, data, format } => {
36-
let format = format.unwrap_or_else(|| "table".to_string());
39+
Command::Parse {
40+
file,
41+
data,
42+
format,
43+
key,
44+
} => {
45+
let key_bytes = parse_key(key.as_deref());
46+
let fmt = format.as_deref().unwrap_or("table");
3747

3848
if let Some(file_path) = file {
3949
let file_content = fs::read_to_string(file_path).expect("Failed to read the file");
40-
print!("{}", serialize_mbus_data(&file_content, &format));
50+
print!(
51+
"{}",
52+
serialize_mbus_data(&file_content, fmt, key_bytes.as_ref())
53+
);
4154
} else if let Some(data_string) = data {
42-
print!("{}", serialize_mbus_data(&data_string, &format));
55+
print!(
56+
"{}",
57+
serialize_mbus_data(&data_string, fmt, key_bytes.as_ref())
58+
);
4359
} else {
4460
eprintln!("Either --file or --data must be provided");
4561
}
4662
}
4763
}
4864
}
4965

66+
fn parse_key(key_hex: Option<&str>) -> Option<[u8; 16]> {
67+
key_hex.and_then(|hex_str| {
68+
hex::decode(hex_str).ok().and_then(|bytes| {
69+
if bytes.len() == 16 {
70+
let mut arr = [0u8; 16];
71+
arr.copy_from_slice(&bytes);
72+
Some(arr)
73+
} else {
74+
eprintln!(
75+
"Warning: Key must be 16 bytes (32 hex chars), got {} bytes. Ignoring key.",
76+
bytes.len()
77+
);
78+
None
79+
}
80+
})
81+
})
82+
}
83+
5084
#[cfg(test)]
5185
mod tests {
5286
use super::*;
5387

5488
#[test]
5589
fn test_parse_data_from_string() {
5690
let data_string = "0x68, 0x3C, 0x3C, 0x68, 0x08, 0x08, 0x72, 0x78, 0x03, 0x49, 0x11, 0x77, 0x04, 0x0E, 0x16, 0x0A, 0x00, 0x00, 0x00, 0x0C, 0x78, 0x78, 0x03, 0x49, 0x11, 0x04, 0x13, 0x31, 0xD4, 0x00, 0x00, 0x42, 0x6C, 0x00, 0x00, 0x44, 0x13, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6D, 0x0B, 0x0B, 0xCD, 0x13, 0x02, 0x27, 0x00, 0x00, 0x09, 0xFD, 0x0E, 0x02, 0x09, 0xFD, 0x0F, 0x06, 0x0F, 0x00, 0x01, 0x75, 0x13, 0xD3, 0x16";
57-
let output = serialize_mbus_data(data_string, "table");
91+
let output = serialize_mbus_data(data_string, "table", None);
5892
assert!(output.contains("Hex"));
5993
}
6094
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "m-bus-application-layer"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[features]
7+
default = []
8+
std = ["m-bus-core/std"]
9+
serde = ["dep:serde", "arrayvec/serde", "bitflags/serde", "m-bus-core/serde"]
10+
defmt = ["dep:defmt", "m-bus-core/defmt"]
11+
decryption = ["m-bus-core/decryption"]
12+
plaintext-before-extension = []
13+
14+
[dependencies]
15+
serde = { version = "1.0", features = ["derive"], optional = true }
16+
defmt = { version = "1.0.1", optional = true }
17+
bitflags = "2.8.0"
18+
arrayvec = { version = "0.7.4", default-features = false }
19+
m-bus-core = { path = "../m-bus-core" }
20+
21+
[dev-dependencies]
22+
wired-mbus-link-layer = { path = "../wired-mbus-link-layer" }

0 commit comments

Comments
 (0)