m-bus-parser
For contributing see CONTRIBUTING.md, for change history see CHANGELOG.md.
An open-source parser (decoder/deserializer) for the wired and wireless M-Bus protocol, written in Rust.
M-Bus (Meter-Bus) is a European standard (EN 13757-2 physical and link layer, EN 13757-3 application layer) for remote reading of water, gas, electricity, and heat meters. — Wikipedia
- Try it live: maebli.github.io/m-bus-parser
- Spec: m-bus.com/documentation · OMS specification
Features
- Parses wired M-Bus (EN 13757-2/-3) and wireless M-Bus (wMBus) frames
- Eight harmonized output formats:
table,json,yaml,csv,mermaid,xml,annotated, andannotated-text - A versioned canonical schema with exact decimal values, provenance, partial-decode diagnostics, and stable error codes
- Responsive, Unicode-aware tables for narrow terminals and browser cards
- AES-128 decryption for encrypted wMBus frames (mode 5 / mode 7)
no_stdcompatible — runs on embedded targets (manufacturer lookup and output formats requirestd)- Available as a Rust library, CLI, WebAssembly (npm) and Python bindings
Stack usage, linked footprint, and decode latency are measured by eagerly
parsing a wired frame and consuming all of its application-layer records with a
pinned compiler and dependency set. The badge above links to the
per-commit resource charts,
including the total critical path and each of its component frames. The
measurement method and local command are documented in
benches/parser-resources/.
Deployments
Web app (WebAssembly)
Paste a hex frame at maebli.github.io/m-bus-parser and get instant output in any format, including a rendered Mermaid diagram. Frames can be shared via URL.
Source: wasm/
CLI
Source: cli/
Python bindings
Source: python/
CLI Usage
m-bus-parser-cli parse [OPTIONS]
Options:
-d, --data <DATA> Raw M-Bus frame as a hex string
-f, --file <FILE> File containing a hex frame
-t, --format <FORMAT> table, json, yaml, csv, mermaid, xml, annotated, annotated-text
-k, --key <KEY> AES-128 decryption key (32 hex characters)
--width <WIDTH> Table width (auto-detected on an interactive terminal)
--no-enrichment Omit manufacturer enrichment
Input hex is strict: use compact hexadecimal or complete byte tokens separated by whitespace, colons, or hyphens.
68 04 04 68 53 01 00 00 54 16 (space-separated)
68040468530100005416 (plain hex)
0x68:0x04:0x04:0x68:0x53:0x01:0x00:0x00:0x54:0x16 (prefixed byte tokens)
Table output (default)
The table automatically selects a wide, compact, or vertical-card layout and
never exceeds the detected terminal width. Use --width 44 to request an exact
maximum explicitly.
Other formats
FRAME="68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16"
# JSON
# YAML
# CSV (one row per input frame; record fields use namespaced columns)
# Colored, semantically grouped Mermaid diagram source (renders in the web app)
# Wired-compatible and wireless XML
# Byte annotations as JSON or human-readable text
# Decrypt an AES-128-encrypted wireless frame
ENCRYPTED_FRAME="2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A"
Library Usage
Add to Cargo.toml:
[]
= { = "0.5", = ["std", "serde"] }
Parse a wired frame
use WiredFrame;
use parse_application_layer;
Parse application-layer data records
When the link and transport headers have already been removed, parse the DIF/VIF records directly:
use ;
Decode and render with typed APIs
use ;
serialize_mbus_data remains as a string compatibility wrapper. New code
should use the typed APIs so invalid input and unsupported options remain
machine-readable OutputError values.
no_std usage
The core parsing types are no_std compatible. Disable default features:
[]
= { = "0.5", = false }
To consume transformed bytes without a frame-sized destination buffer:
FormatAFrame::new(data)?.bytes()borrows a wireless Format A frame, skips recognized CRCs, and yields its corrected length byte. Construction scans the blocks to determine the normalized length; iteration does not copy the frame.- With
decryption,decryption::EncryptedPayload::decrypted_bytes(&keys)yields plaintext lazily. Construct the payload with ciphertext and an explicitdecryption::KeyContextderived from the parsed headers. Key lookup and cipher state stay in the core decryption module. AES-CBC retains a key schedule, chaining state, and one 16-byte working block. With the current software AES backend, the iterator occupies 760 bytes onthumbv7m-none-eabi; the decryption-only cipher shares that backend's key schedule. Incomplete trailing blocks retain the existing pass-through behavior.
These APIs are allocation-free. Existing *_into/decrypt_variable_data* and
strip_format_a_crcs APIs remain available when contiguous output is needed.
CRC stripping scans each block once, and unencrypted decrypt_into calls copy
bytes directly without constructing AES iterator state.
The existing frame/record parsers take &[u8], so they still require contiguous
normalized/plaintext storage; they do not directly accept these byte iterators.
Small decoded header/scalar values are still stored by value.
An embedded example (Cortex-M) is in examples/cortex-m/.
Testing
Run these commands from the repository root:
# Test the default no_std-compatible configuration
# Test the std-enabled APIs and integration tests
# Test every optional feature, including serde and decryption
# Test the alternate plaintext-before-extension behavior
The first command uses the host test harness but builds the parser without its
std feature. To also verify the library on a bare-metal target:
Run the Cortex-M QEMU demo
Install qemu-system-arm, make sure it is available on PATH, and add the
target with rustup target add thumbv7m-none-eabi. Then run the demo from the
repository root with this one-liner:
( && )
The example parses a frame, prints the result through semihosting, and exits QEMU.
Output Formats
| Format | Flag | Description |
|---|---|---|
table |
default | Width-aware human-readable table |
json |
-t json |
Canonical schema as JSON |
yaml |
-t yaml |
Canonical schema as YAML |
csv |
-t csv |
One frame row with namespaced record columns |
mermaid |
-t mermaid |
Colored, layer-oriented Mermaid flowchart |
xml |
-t xml |
Wired libmbus-compatible and wireless XML |
annotated |
-t annotated |
Byte-segment annotation envelope |
annotated-text |
-t annotated-text |
Human-readable byte annotations |
Naming and interoperability
The canonical JSON, YAML, CSV, table, Mermaid, Python, and WebAssembly outputs share one vocabulary:
- JSON/YAML member names and annotation identifiers use
snake_case. - Link-layer functions use the M-Bus mnemonics (
RSP_UD,REQ_UD2,SND_NKE). Record functions, quantities, and data codings use the terms from the M-Bus application-layer tables, such asInstantaneous value,Volume flow, and6-digit BCD. unitis a single case-sensitive UCUM expression when one is available, such asW,Cel, orm3.h-1.- Complete temporal values use ISO 8601 notation. The parser does not invent a timezone when a meter does not transmit one.
- Each record value contains a
kindand at most one parsedvalue. Exact decimals, text, and complete temporal values use strings; finite floats use JSON numbers; partial temporal values use a compact component object. - Raw binary values use uppercase hexadecimal and fields containing them end
in
_hex. Record bytes live only inheader_hexanddata_hex.
There is no common JSON schema shared by M-Bus parsers. These rules retain the
protocol vocabulary used by M-Bus
and libmbus while keeping the structured formats predictable. The xml format
deliberately retains libmbus's established XML vocabulary and unit symbols for
compatibility. Formats whose names end in -legacy retain their historical
contracts.
Protocol Coverage
Frame types
| Type | CI bytes | Status |
|---|---|---|
| Long frame | 0x72, 0x76, 0x7A | Supported |
| Short frame | — | Supported |
| Control frame | — | Supported |
| Single character | — | Supported |
| Wireless frame | wMBus link layer | Supported |
CI field types
Implemented
ResponseWithVariableDataStructure(CI: 0x72, 0x76, 0x7A)ResponseWithFixedDataStructure(CI: 0x73)ApplicationLayerShortTransport(CI: 0x7D)ApplicationLayerLongTransport(CI: 0x7E)ExtendedLinkLayerI(CI: 0x8A)ResetAtApplicationLevel
Not yet implemented
Returns ApplicationLayerError::Unimplemented for: SendData, SelectSlave, SynchronizeSlave, baud-rate commands, ExtendedLinkLayerII/III, COSEM/OBIS data, and various transport/network layer types.
Most common value information unit codes are supported. Contributions for additional CI types and VIF codes are welcome.
Frame Structure
Wireless Link Layer

Wired Link Layer (Long Frame)

Application Layer

Value Information Block

Related Projects
| Language | Project |
|---|---|
| C | libmbus by rscada |
| Java | jMbus |
| C# | Valley.Net.Protocols.MeterBus |
| JS | tmbus |
| Python | pyMeterBus |
