Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]


## [0.1.0]

- wmbus parsing capabilities
- preperation for decryption
- breaking changes to API for using the lib
- refacatoring things into core to be shared by wireless and wired parsing parts
24 changes: 18 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "m-bus-parser"
version = "0.0.28"
version = "0.1.0"
edition = "2021"
description = "A library for parsing M-Bus frames"
license = "MIT"
Expand All @@ -18,16 +18,18 @@ hex = "0.4"
serde = "1.0.217"
serde_derive = "1.0.217"
serde-xml-rs = "0.7.0"
serde_json = "1.0"

[build-dependencies]
bindgen = "0.72.0"

[features]
default = []
std = ["prettytable-rs", "serde_json", "serde_yaml", "serde"]
plaintext-before-extension = []
serde = ["dep:serde", "arrayvec/serde", "bitflags/serde"]
defmt = ["dep:defmt"]
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"]
plaintext-before-extension = ["m-bus-application-layer/plaintext-before-extension"]
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"]
defmt = ["dep:defmt", "wired-mbus-link-layer/defmt", "wireless-mbus-link-layer/defmt", "m-bus-core/defmt", "m-bus-application-layer/defmt"]
decryption = ["dep:aes", "dep:cbc", "dep:cipher", "dep:aes-gcm", "dep:ccm", "m-bus-application-layer/decryption"]

[profile.release]
opt-level = 'z' # Optimize for size
Expand All @@ -45,8 +47,18 @@ serde = { version = "1.0", features = ["derive"], optional = true }
bitflags = "2.8.0"
arrayvec = { version = "0.7.4", default-features = false }
defmt = { version = "1.0.1", optional = true }
wired-mbus-link-layer = {path = "crates/wired-mbus-link-layer"}
wireless-mbus-link-layer = {path = "crates/wireless-mbus-link-layer"}
m-bus-core = {path = "crates/m-bus-core"}
m-bus-application-layer = {path = "crates/m-bus-application-layer"}
aes = { version = "0.8", optional = true, default-features = false }
cbc = { version = "0.1", optional = true, default-features = false }
cipher = { version = "0.4", optional = true, default-features = false, features = ["block-padding"] }
aes-gcm = { version = "0.10", optional = true, default-features = false, features = ["aes"] }
ccm = { version = "0.5", optional = true, default-features = false }

[workspace]
members = ["cli", "wasm","python"]
members = ["cli", "wasm","python", "crates/m-bus-application-layer", "crates/wired-mbus-link-layer", "crates/wireless-mbus-link-layer", "crates/m-bus-core"]
exclude = ["examples/cortex-m"]

[[bench]]
Expand Down
80 changes: 56 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@

# m-bus-parser (wired)
# m-bus-parser

[![Discord](https://img.shields.io/badge/Discord-Join%20Now-blue?style=flat&logo=Discord)](https://discord.gg/FfmecQ4wua)
[![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)


### Introduction

*For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md)*
*For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md), for change history see [CHANGELOG.md](./CHANGELOG.md),*

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.
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.

"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)

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.

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).

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.

## Supported Features

### Control Information Types

The parser currently supports the following Control Information (CI) types:

#### Implemented
- **ResetAtApplicationLevel** - Application layer reset
- **ResponseWithVariableDataStructure** - Variable data response (CI: 0x72, 0x76, 0x7A)
- **ResponseWithFixedDataStructure** - Fixed data response (CI: 0x73)
- **ApplicationLayerShortTransport** - Short transport layer frame (CI: 0x7D)
- **ApplicationLayerLongTransport** - Long transport layer frame (CI: 0x7E)
- **ExtendedLinkLayerI** - Extended link layer type I (CI: 0x8A)

#### Not Yet Implemented
The following CI types will return an `ApplicationLayerError::Unimplemented` error:
- SendData, SelectSlave, SynchronizeSlave
- SetBaudRate* (300, 600, 1200, 2400, 4800, 9600, 19200, 38400)
- OutputRAMContent, WriteRAMContent
- StartCalibrationTestMode, ReadEEPROM, StartSoftwareTest
- HashProcedure, SendErrorStatus, SendAlarmStatus
- DataSentWith*TransportLayer, CosemData*, ObisData*
- ApplicationLayerFormatFrame*, ClockSync*
- ApplicationError*, Alarm*, NetworkLayer*
- TransportLayer* (various types)
- ExtendedLinkLayerII, ExtendedLinkLayerIII

For a complete list, refer to EN 13757-3 specification.

### Value Information Units

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.
Most common value information unit codes are supported. Some specialized units may return `DataInformationError::Unimplemented`:
- Reserved length values in variable length data
- Special functions data parsing
- Partial primary and extended value information unit codes

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

## Dependants and Deployments

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

### Visualization of Library Function

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/).
## Wireless Link Layer

![](./resources/wireless-frame.png)

## Wired Link Layer

The most common wired frame is the `LongFrame`.

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

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

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

## Aim

- suitable for embedded targets `no_std`
- Follow the Rust API Guideline https://rust-lang.github.io/api-guidelines/
- minimal copy

## Development status

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.

## Example of current function
## Simple example, parsing wired m bus frame

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

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

```rust

use m_bus_parser::frames::{Address, Frame, Function};

let example = vec![
0x68, 0x06, 0x06, 0x68,
0x53, 0xFE, 0x51,
0x01, 0x7A, 0x08,
use m_bus_parser::{Address, WiredFrame, Function};

let example = vec![
0x68, 0x06, 0x06, 0x68,
0x53, 0xFE, 0x51,
0x01, 0x7A, 0x08,
0x25, 0x16,
];

let frame = Frame::try_from(example.as_slice()))?;
let frame = WiredFrame::try_from(example.as_slice())?;

if let Frame::ControlFrame { function, address, data } = frame {
if let WiredFrame::ControlFrame { function, address, data } = frame {
assert_eq!(address, Address::Broadcast { reply_required: true });
assert_eq!(function, Function::SndUd { fcb: (false)});
assert_eq!(data, &[0x51,0x01, 0x7A, 0x08]);
Expand Down
16 changes: 10 additions & 6 deletions benches/bench.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use m_bus_parser::frames::Frame;
use criterion::{criterion_group, criterion_main, Criterion};
use m_bus_parser::mbus_data::MbusData;
use m_bus_parser::WiredFrame;
use std::hint::black_box;

#[allow(clippy::unwrap_used)]
fn frame_parse_benchmark(c: &mut Criterion) {
let data = vec![0x68, 0x04, 0x04, 0x68, 0x53, 0x01, 0x00, 0x00, 0x54, 0x16];
let data: Vec<u8> = vec![0x68, 0x04, 0x04, 0x68, 0x53, 0x01, 0x00, 0x00, 0x54, 0x16];
c.bench_function("parse_frame_only", |b| {
b.iter(|| {
// Use black_box to prevent compiler optimizations from skipping the computation
Frame::try_from(black_box(data.as_slice())).unwrap();
WiredFrame::try_from(black_box(data.as_slice())).unwrap();
})
});
}

#[allow(clippy::unwrap_used)]
fn m_bus_parser_benchmark(c: &mut Criterion) {
let data = vec![
let data: Vec<u8> = vec![
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,
Expand All @@ -21,7 +25,7 @@ fn m_bus_parser_benchmark(c: &mut Criterion) {
];
c.bench_function("parse", |b| {
b.iter(|| {
m_bus_parser::MbusData::try_from(data.as_slice()).unwrap();
MbusData::<WiredFrame>::try_from(data.as_slice()).unwrap();
})
});
}
Expand Down
8 changes: 5 additions & 3 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "m-bus-parser-cli"
version = "0.0.16"
version = "0.1.0"
edition = "2021"
description = "A cli to use the library for parsing M-Bus frames"
license = "MIT"
Expand All @@ -18,8 +18,10 @@ tag-name = "cli-v{{version}}"
[build-dependencies]

[features]

default = ["decryption"]
decryption = ["m-bus-parser/decryption"]

[dependencies]
m-bus-parser = { path = "..", version = "0.0.28", features = ["std", "serde"] }
m-bus-parser = { path = "..", version = "0.1.0", features = ["std", "serde"] }
hex = "0.4"
clap = { version = "4.5.4", features = ["derive"] }
2 changes: 1 addition & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Long Frame
├───────────────────────┼────────────────────────────────────────────┤
│ Version │ 2 │
├───────────────────────┼────────────────────────────────────────────┤
Medium │ Heat
Device Type │ Heat Meter
└───────────────────────┴────────────────────────────────────────────┘
┌──────────────────────────────────────────┬───────────────────────┬─────────────┐
│ Value │ Data Information │ Hex │
Expand Down
44 changes: 39 additions & 5 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,70 @@ enum Command {

#[arg(short = 't', long)]
format: Option<String>,

/// Decryption key (32 hex characters for AES-128)
#[arg(short = 'k', long)]
key: Option<String>,
},
}

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

match cli.command {
Command::Parse { file, data, format } => {
let format = format.unwrap_or_else(|| "table".to_string());
Command::Parse {
file,
data,
format,
key,
} => {
let key_bytes = parse_key(key.as_deref());
let fmt = format.as_deref().unwrap_or("table");

if let Some(file_path) = file {
let file_content = fs::read_to_string(file_path).expect("Failed to read the file");
print!("{}", serialize_mbus_data(&file_content, &format));
print!(
"{}",
serialize_mbus_data(&file_content, fmt, key_bytes.as_ref())
);
} else if let Some(data_string) = data {
print!("{}", serialize_mbus_data(&data_string, &format));
print!(
"{}",
serialize_mbus_data(&data_string, fmt, key_bytes.as_ref())
);
} else {
eprintln!("Either --file or --data must be provided");
}
}
}
}

fn parse_key(key_hex: Option<&str>) -> Option<[u8; 16]> {
key_hex.and_then(|hex_str| {
hex::decode(hex_str).ok().and_then(|bytes| {
if bytes.len() == 16 {
let mut arr = [0u8; 16];
arr.copy_from_slice(&bytes);
Some(arr)
} else {
eprintln!(
"Warning: Key must be 16 bytes (32 hex chars), got {} bytes. Ignoring key.",
bytes.len()
);
None
}
})
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_data_from_string() {
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";
let output = serialize_mbus_data(data_string, "table");
let output = serialize_mbus_data(data_string, "table", None);
assert!(output.contains("Hex"));
}
}
1 change: 1 addition & 0 deletions crates/m-bus-application-layer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
22 changes: 22 additions & 0 deletions crates/m-bus-application-layer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "m-bus-application-layer"
version = "0.1.0"
edition = "2021"

[features]
default = []
std = ["m-bus-core/std"]
serde = ["dep:serde", "arrayvec/serde", "bitflags/serde", "m-bus-core/serde"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serde feature should imply std

defmt = ["dep:defmt", "m-bus-core/defmt"]
decryption = ["m-bus-core/decryption"]
plaintext-before-extension = []

[dependencies]
serde = { version = "1.0", features = ["derive"], optional = true }
defmt = { version = "1.0.1", optional = true }
bitflags = "2.8.0"
arrayvec = { version = "0.7.4", default-features = false }
m-bus-core = { path = "../m-bus-core" }

[dev-dependencies]
wired-mbus-link-layer = { path = "../wired-mbus-link-layer" }
Loading
Loading