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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Added direct application-layer parsing APIs, record accessors, and a crate-local data-record example.
- Moved full-frame application-layer coverage to the top-level crate.
- Fixed data-record iteration to report malformed records instead of silently stopping.
- Fixed mojibake in decrypted variable-length UTF-8 text while preserving ISO-8859-1 fallback decoding.

## [0.1.3] - 2026-06-11
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ m-bus-parser = "0.1"
```rust
use m_bus_parser::{Address, WiredFrame, Function};
use m_bus_parser::mbus_data::MbusData;
use m_bus_parser::user_data::{DataRecords, UserDataBlock};
use m_bus_parser::user_data::parse_application_layer;

let frame_bytes: Vec<u8> = vec![
0x68, 0x4D, 0x4D, 0x68, 0x08, 0x01, 0x72, 0x01,
Expand All @@ -174,21 +174,31 @@ let frame_bytes: Vec<u8> = vec![
let frame = WiredFrame::try_from(frame_bytes.as_slice())?;

if let WiredFrame::LongFrame { function, address, data } = frame {
if let Ok(user_data) = UserDataBlock::try_from(data) {
if let UserDataBlock::VariableDataStructureWithLongTplHeader {
long_tpl_header,
variable_data_block,
..
} = user_data {
let records = DataRecords::from((variable_data_block, &long_tpl_header));
for record in records.flatten() {
println!("{}", record.data);
}
let application_layer = parse_application_layer(data)?;
if let Some(records) = application_layer.data_records() {
for record in records {
println!("{:?}", record?.value());
}
}
}
```

### Parse application-layer data records

When the link and transport headers have already been removed, parse the DIF/VIF
records directly:

```rust
use m_bus_parser::user_data::parse_data_records;

let data = [0x03, 0x13, 0x15, 0x31, 0x00];
for record in parse_data_records(&data) {
let record = record?;
println!("value: {:?}", record.value());
println!("value information: {:?}", record.value_information());
}
```

### Serialize to any format

```rust
Expand Down
4 changes: 1 addition & 3 deletions crates/m-bus-application-layer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"
description = "M-Bus application layer parser (DIF/VIF, data records)"
license = "MIT"
repository = "https://github.com/maebli/m-bus-parser"
readme = "README.md"

[features]
default = []
Expand All @@ -20,6 +21,3 @@ defmt = { version = "1.0.1", optional = true }
bitflags = "2.8.0"
arrayvec = { version = "0.7.4", default-features = false }
m-bus-core = { version = "0.1.3", path = "../m-bus-core" }

[dev-dependencies]
wired-mbus-link-layer = { path = "../wired-mbus-link-layer" }
28 changes: 28 additions & 0 deletions crates/m-bus-application-layer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# M-Bus application layer

This crate parses EN 13757-3 application-layer data without requiring a wired
or wireless link-layer frame.

Use `parse_application_layer` when the input begins with a CI field, or
`parse_data_records` when the CI and transport headers have already been
removed:

```rust
use m_bus_application_layer::parse_data_records;

let data = [0x03, 0x13, 0x15, 0x31, 0x00];
for record in parse_data_records(&data) {
let record = record?;
println!("value: {:?}", record.value());
println!("value information: {:?}", record.value_information());
}
```

Run the complete data-record example from the workspace root:

```console
cargo run -p m-bus-application-layer --example parse_data_records
```

The parser is allocation-free and supports `no_std`; formatting and standard
error traits are enabled by the optional `std` feature.
24 changes: 24 additions & 0 deletions crates/m-bus-application-layer/examples/parse_data_records.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use m_bus_application_layer::{parse_data_records, DataRecordError};

fn main() -> Result<(), DataRecordError> {
// Application-layer records only: no wired/wireless frame or CI/TPL header.
let data = [
0x03, 0x13, 0x15, 0x31, 0x00, // Volume: 12_565 x 10^-3 m³
0x02, 0x5A, 0xD7, 0x04, // Flow temperature: 1_239 x 10^-1 °C
];

for (index, record) in parse_data_records(&data).enumerate() {
let record = record?;

println!("Record {}", index + 1);
println!(" value: {:?}", record.value());
if let Some(value_information) = record.value_information() {
println!(" labels: {:?}", value_information.labels);
println!(" scale: 10^{}", value_information.decimal_scale_exponent);
println!(" units: {:?}", value_information.units);
}
println!(" raw bytes: {:02X?}", record.raw_bytes());
}

Ok(())
}
30 changes: 30 additions & 0 deletions crates/m-bus-application-layer/src/data_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@ pub struct DataRecord<'a> {
}

impl DataRecord<'_> {
/// Returns the parsed value carried by this record.
#[must_use]
pub fn value(&self) -> Option<&DataType<'_>> {
self.data.value.as_ref()
}

/// Returns the processed data information (DIF and DIFE fields).
#[must_use]
pub fn data_information(&self) -> Option<&DataInformation> {
self.data_record_header
.processed_data_record_header
.data_information
.as_ref()
}

/// Returns the processed value information (VIF and VIFE fields).
#[must_use]
pub fn value_information(&self) -> Option<&ValueInformation> {
self.data_record_header
.processed_data_record_header
.value_information
.as_ref()
}

/// Returns all raw bytes consumed by this record.
#[must_use]
pub fn raw_bytes(&self) -> &[u8] {
self.raw_bytes
}

#[must_use]
pub fn get_size(&self) -> usize {
self.raw_bytes.len()
Expand Down
4 changes: 2 additions & 2 deletions crates/m-bus-application-layer/src/extended_link_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ pub struct EncryptionFields {
pub enum EllFormat {
/// Extended Link Layer I (2 bytes: CC, ACC)
FormatI,
/// Extended Link Layer II (8 bytes: CC, ACC, SN[4], CRC[2])
/// Extended Link Layer II (8 bytes: CC, ACC, `SN[4]`, `CRC[2]`)
FormatII,
/// Extended Link Layer III (16 bytes: CC, ACC, MFR[2], ADDR[6], SN[4], CRC[2])
/// Extended Link Layer III (16 bytes: CC, ACC, `MFR[2]`, `ADDR[6]`, `SN[4]`, `CRC[2]`)
FormatIII,
}

Expand Down
Loading
Loading