Skip to content

Commit be40d3b

Browse files
authored
Merge pull request #104 from maebli/feature/application-layer-parsing
Improve application layer parsing API
2 parents ccaad33 + f670459 commit be40d3b

12 files changed

Lines changed: 409 additions & 214 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

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

1215
## [0.1.3] - 2026-06-11

README.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ m-bus-parser = "0.1"
164164
```rust
165165
use m_bus_parser::{Address, WiredFrame, Function};
166166
use m_bus_parser::mbus_data::MbusData;
167-
use m_bus_parser::user_data::{DataRecords, UserDataBlock};
167+
use m_bus_parser::user_data::parse_application_layer;
168168

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

176176
if let WiredFrame::LongFrame { function, address, data } = frame {
177-
if let Ok(user_data) = UserDataBlock::try_from(data) {
178-
if let UserDataBlock::VariableDataStructureWithLongTplHeader {
179-
long_tpl_header,
180-
variable_data_block,
181-
..
182-
} = user_data {
183-
let records = DataRecords::from((variable_data_block, &long_tpl_header));
184-
for record in records.flatten() {
185-
println!("{}", record.data);
186-
}
177+
let application_layer = parse_application_layer(data)?;
178+
if let Some(records) = application_layer.data_records() {
179+
for record in records {
180+
println!("{:?}", record?.value());
187181
}
188182
}
189183
}
190184
```
191185

186+
### Parse application-layer data records
187+
188+
When the link and transport headers have already been removed, parse the DIF/VIF
189+
records directly:
190+
191+
```rust
192+
use m_bus_parser::user_data::parse_data_records;
193+
194+
let data = [0x03, 0x13, 0x15, 0x31, 0x00];
195+
for record in parse_data_records(&data) {
196+
let record = record?;
197+
println!("value: {:?}", record.value());
198+
println!("value information: {:?}", record.value_information());
199+
}
200+
```
201+
192202
### Serialize to any format
193203

194204
```rust

crates/m-bus-application-layer/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
description = "M-Bus application layer parser (DIF/VIF, data records)"
66
license = "MIT"
77
repository = "https://github.com/maebli/m-bus-parser"
8+
readme = "README.md"
89

910
[features]
1011
default = []
@@ -20,6 +21,3 @@ defmt = { version = "1.0.1", optional = true }
2021
bitflags = "2.8.0"
2122
arrayvec = { version = "0.7.4", default-features = false }
2223
m-bus-core = { version = "0.1.3", path = "../m-bus-core" }
23-
24-
[dev-dependencies]
25-
wired-mbus-link-layer = { path = "../wired-mbus-link-layer" }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# M-Bus application layer
2+
3+
This crate parses EN 13757-3 application-layer data without requiring a wired
4+
or wireless link-layer frame.
5+
6+
Use `parse_application_layer` when the input begins with a CI field, or
7+
`parse_data_records` when the CI and transport headers have already been
8+
removed:
9+
10+
```rust
11+
use m_bus_application_layer::parse_data_records;
12+
13+
let data = [0x03, 0x13, 0x15, 0x31, 0x00];
14+
for record in parse_data_records(&data) {
15+
let record = record?;
16+
println!("value: {:?}", record.value());
17+
println!("value information: {:?}", record.value_information());
18+
}
19+
```
20+
21+
Run the complete data-record example from the workspace root:
22+
23+
```console
24+
cargo run -p m-bus-application-layer --example parse_data_records
25+
```
26+
27+
The parser is allocation-free and supports `no_std`; formatting and standard
28+
error traits are enabled by the optional `std` feature.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use m_bus_application_layer::{parse_data_records, DataRecordError};
2+
3+
fn main() -> Result<(), DataRecordError> {
4+
// Application-layer records only: no wired/wireless frame or CI/TPL header.
5+
let data = [
6+
0x03, 0x13, 0x15, 0x31, 0x00, // Volume: 12_565 x 10^-3 m³
7+
0x02, 0x5A, 0xD7, 0x04, // Flow temperature: 1_239 x 10^-1 °C
8+
];
9+
10+
for (index, record) in parse_data_records(&data).enumerate() {
11+
let record = record?;
12+
13+
println!("Record {}", index + 1);
14+
println!(" value: {:?}", record.value());
15+
if let Some(value_information) = record.value_information() {
16+
println!(" labels: {:?}", value_information.labels);
17+
println!(" scale: 10^{}", value_information.decimal_scale_exponent);
18+
println!(" units: {:?}", value_information.units);
19+
}
20+
println!(" raw bytes: {:02X?}", record.raw_bytes());
21+
}
22+
23+
Ok(())
24+
}

crates/m-bus-application-layer/src/data_record.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,36 @@ pub struct DataRecord<'a> {
2929
}
3030

3131
impl DataRecord<'_> {
32+
/// Returns the parsed value carried by this record.
33+
#[must_use]
34+
pub fn value(&self) -> Option<&DataType<'_>> {
35+
self.data.value.as_ref()
36+
}
37+
38+
/// Returns the processed data information (DIF and DIFE fields).
39+
#[must_use]
40+
pub fn data_information(&self) -> Option<&DataInformation> {
41+
self.data_record_header
42+
.processed_data_record_header
43+
.data_information
44+
.as_ref()
45+
}
46+
47+
/// Returns the processed value information (VIF and VIFE fields).
48+
#[must_use]
49+
pub fn value_information(&self) -> Option<&ValueInformation> {
50+
self.data_record_header
51+
.processed_data_record_header
52+
.value_information
53+
.as_ref()
54+
}
55+
56+
/// Returns all raw bytes consumed by this record.
57+
#[must_use]
58+
pub fn raw_bytes(&self) -> &[u8] {
59+
self.raw_bytes
60+
}
61+
3262
#[must_use]
3363
pub fn get_size(&self) -> usize {
3464
self.raw_bytes.len()

crates/m-bus-application-layer/src/extended_link_layer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ pub struct EncryptionFields {
3030
pub enum EllFormat {
3131
/// Extended Link Layer I (2 bytes: CC, ACC)
3232
FormatI,
33-
/// Extended Link Layer II (8 bytes: CC, ACC, SN[4], CRC[2])
33+
/// Extended Link Layer II (8 bytes: CC, ACC, `SN[4]`, `CRC[2]`)
3434
FormatII,
35-
/// Extended Link Layer III (16 bytes: CC, ACC, MFR[2], ADDR[6], SN[4], CRC[2])
35+
/// Extended Link Layer III (16 bytes: CC, ACC, `MFR[2]`, `ADDR[6]`, `SN[4]`, `CRC[2]`)
3636
FormatIII,
3737
}
3838

0 commit comments

Comments
 (0)