diff --git a/CHANGELOG.md b/CHANGELOG.md index 492419c..0802f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New `xml` output format that reproduces the legacy rSCADA/libmbus + `mbus_frame_data_xml_normalized()` output byte for byte, as a drop-in + replacement for consumers of libmbus's normalized XML. Available through + `serialize_mbus_data(hex, "xml", None)`, the CLI (`-t xml`), and the Python + bindings. +- Parity test (`tests/rscada_xml.rs`) diffing the `xml` output against the + reference `.norm.xml` files in `tests/rscada`. + +### Fixed + +- VIF plaintext extension chains are now walked from the correct offset when + the plaintext VIF precedes the VIFE bytes (`plaintext-before-extension`), + so records that previously aborted with `InvalidValueInformation` now parse. +- A 6-byte data field carrying a date/time VIF (0x6D) is now decoded as a + type I (CP48) timestamp instead of type F, matching EN 13757-3. + ## [0.2.0] Breaking changes for JSON/YAML consumers: diff --git a/README.md b/README.md index 04a0afb..8030e82 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ An open-source parser (decoder/deserializer) for the **wired** and **wireless** ## Features - Parses **wired M-Bus** (EN 13757-2/-3) and **wireless M-Bus** (wMBus) frames -- **Five output formats**: `table`, `json`, `yaml`, `csv`, `mermaid` +- **Six output formats**: `table`, `json`, `yaml`, `csv`, `mermaid`, `xml` - **AES-128 decryption** for encrypted wMBus frames (mode 5 / mode 7) - **`no_std` compatible** — runs on embedded targets (manufacturer lookup and output formats require `std`) - Available as a **Rust library**, **CLI**, **WebAssembly (npm)** and **Python bindings** @@ -69,7 +69,7 @@ m-bus-parser-cli parse [OPTIONS] Options: -d, --data Raw M-Bus frame as a hex string -f, --file File containing a hex frame - -t, --format Output format: table (default), json, yaml, csv, mermaid + -t, --format Output format: table (default), json, yaml, csv, mermaid, xml -k, --key AES-128 decryption key (32 hex characters) ``` @@ -144,6 +144,9 @@ m-bus-parser-cli parse -d "..." -t csv # Mermaid diagram source (renders in the web app) m-bus-parser-cli parse -d "..." -t mermaid +# Legacy libmbus normalized XML (drop-in for rSCADA/libmbus consumers) +m-bus-parser-cli parse -d "..." -t xml + # With AES-128 decryption key m-bus-parser-cli parse -d "..." -k "000102030405060708090A0B0C0D0E0F" ``` @@ -211,6 +214,7 @@ let json = serialize_mbus_data(hex, "json", None); let yaml = serialize_mbus_data(hex, "yaml", None); let csv = serialize_mbus_data(hex, "csv", None); let mermaid = serialize_mbus_data(hex, "mermaid", None); +let xml = serialize_mbus_data(hex, "xml", None); // With decryption key let key: [u8; 16] = [0x00, 0x01, ..., 0x0F]; @@ -239,6 +243,7 @@ An embedded example (Cortex-M) is in [`examples/cortex-m/`](./examples/cortex-m) | `yaml` | `-t yaml` | YAML | | `csv` | `-t csv` | Comma-separated values | | `mermaid` | `-t mermaid` | Mermaid flowchart source (renders in web app)| +| `xml` | `-t xml` | Legacy libmbus normalized XML (rSCADA-compatible)| --- diff --git a/cli/src/main.rs b/cli/src/main.rs index 824953b..915d22a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -23,6 +23,7 @@ enum Command { #[arg(short = 'd', long)] data: Option, + /// Output format: table (default), json, yaml, csv, mermaid, xml #[arg(short = 't', long)] format: Option, diff --git a/crates/m-bus-application-layer/src/data_record.rs b/crates/m-bus-application-layer/src/data_record.rs index 7c005ca..a450d86 100644 --- a/crates/m-bus-application-layer/src/data_record.rs +++ b/crates/m-bus-application-layer/src/data_record.rs @@ -205,7 +205,13 @@ impl TryFrom<&RawDataRecordHeader<'_>> for ProcessedDataRecordHeader { if v.labels.contains(&ValueLabel::Date) { d.data_field_coding = DataFieldCoding::DateTypeG; } else if v.labels.contains(&ValueLabel::DateTime) { - d.data_field_coding = DataFieldCoding::DateTimeTypeF; + // VIF 0x6D with a 6-byte data field is a type I date and time + // (EN 13757-3), only the 4-byte variant is type F. + d.data_field_coding = if d.data_field_coding == DataFieldCoding::Integer48Bit { + DataFieldCoding::DateTimeTypeI + } else { + DataFieldCoding::DateTimeTypeF + }; } else if v.labels.contains(&ValueLabel::Time) { d.data_field_coding = DataFieldCoding::DateTimeTypeJ; } else if v.labels.contains(&ValueLabel::DateTimeWithSeconds) { diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 2d86fae..1675d9d 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -49,7 +49,12 @@ impl TryFrom<&[u8]> for ValueInformationBlock { } if vif.has_extension() { - let mut offset = 1; + // When the plaintext VIF precedes the extensions, the VIFE chain + // starts after the ASCII length byte and string, not at offset 1. + let mut offset = match &plaintext_vife { + Some(chars) if !standard_plaintex_vib => 1 + 1 + chars.len(), + _ => 1, + }; while offset < data.len() { let vife_data = *data.get(offset).ok_or(DataInformationError::DataTooShort)?; let current_vife = ValueInformationFieldExtension { data: vife_data }; diff --git a/docs/index.html b/docs/index.html index e6e5498..f9a1df2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -669,6 +669,7 @@

Online M-Bus Parser +
@@ -688,7 +689,7 @@

Online M-Bus Parser