A modern .NET 10 library for M-Bus (Meter Bus) communication and frame parsing over TCP, UDP, and serial. Implements the EN 13757-2 (physical and link layer) and EN 13757-3 (application layer) standards.
M-Bus (Meter-Bus) is a European standard for the remote reading of utility meters such as gas, water, and electricity. It is designed for cost-effective two-wire communication and supports both wired (EN 13757-2/3) and wireless (EN 13757-4) variants.
Typical use cases include:
- Remote reading of utility meters in residential and commercial buildings
- Centralized data collection via gateways or hand-held readers
- Alarm systems, heating control, and building automation
┌──────────────────────────────────────────────────────────┐
│ IMBusMaster │
│ PingAsync, RequestDataAsync, ScanAsync, etc. │
├──────────────────────────────────────────────────────────┤
│ IPacketMapper │ IFrameParser / IFrameSerializer│
│ LongFrame → Packet │ bytes ↔ MBusFrame records │
├──────────────────────────────────────────────────────────┤
│ IMBusTransport │
│ ConnectAsync, SendFrameAsync, ReceiveFrameAsync │
├──────────┬──────────────┬────────────────────────────────┤
│ TCP │ UDP │ Serial │
│ Pipes │ Socket │ System.IO.Ports + PipeReader │
└──────────┴──────────────┴────────────────────────────────┘
| Project | Description |
|---|---|
Valley.Net.Protocols.MeterBus.Abstractions |
Interfaces, record types, enums -- zero dependencies |
Valley.Net.Protocols.MeterBus |
Core implementation: FrameParser, FrameSerializer, PacketMapper, VifLookupService, MBusMaster |
Valley.Net.Protocols.MeterBus.Transport.Tcp |
TCP transport using System.IO.Pipelines |
Valley.Net.Protocols.MeterBus.Transport.Udp |
UDP transport using Socket |
Valley.Net.Protocols.MeterBus.Transport.Serial |
Serial transport wrapping System.IO.Ports with PipeReader |
- .NET 10 SDK or later
dotnet add package Valley.Net.Protocols.MeterBus
dotnet add package Valley.Net.Protocols.MeterBus.Transport.Tcp # or .Udp / .Serialservices.AddMBusCore();
services.AddSingleton<IMBusTransport>(sp =>
new TcpMBusTransport("192.168.1.135", 502));await using var transport = new TcpMBusTransport("192.168.1.135", 502);
await transport.ConnectAsync();
await using var master = new MBusMaster(
transport,
new FrameParser(),
new FrameSerializer(),
new PacketMapper(new VifLookupService()));
var packet = await master.RequestDataAsync(0x0a);
if (packet is VariableDataPacket vdp)
{
Console.WriteLine($"Device: {vdp.DeviceType}, Records: {vdp.Records.Length}");
foreach (var record in vdp.Records)
Console.WriteLine($" {record.Units[0].Quantity}: {record.Value}");
}var addresses = Enumerable.Range(0, 250).Select(i => (byte)i);
await foreach (var meter in master.ScanAsync(addresses))
{
Console.WriteLine($"Found meter at address {meter.Address}");
}var parser = new FrameParser();
var mapper = new PacketMapper(new VifLookupService());
var bytes = "68 31 31 68 08 01 72 45 58 57 03 B4 05 ..."
.HexToBytes();
var frame = parser.Parse(bytes);
if (frame.IsSuccess)
{
var packet = mapper.MapToPacket(frame.Value!);
// Use packet...
}- Immutable data -- All frames and packets are C#
recordtypes - Explicit errors --
MBusParseResult<T>instead of exceptions for parsing - Async-first --
CancellationTokeneverywhere,IAsyncEnumerablefor scanning - Zero external dependencies -- Abstractions project has no NuGet dependencies
- Dependency Injection -- All services are injectable via
IServiceCollection.AddMBusCore() - Span-based parsing --
ReadOnlySpan<byte>for zero-allocation frame parsing
dotnet restore Valley.Net.Protocols.MeterBus.sln
dotnet build Valley.Net.Protocols.MeterBus.sln --configuration Release
dotnet test Valley.Net.Protocols.MeterBus.sln --configuration Release- Full architectural rewrite -- Multi-project solution with clean separation of concerns
- Replaced
Valley.Net.Bindingsdependency with nativeIMBusTransportabstraction - Immutable
recordtypes for all frames (MBusFrame) and packets (MBusPacket) MBusParseResult<T>result type for explicit success/failure instead of exceptionsReadOnlySpan<byte>-basedFrameParserreplacingBinaryReader-basedMeterbusFrameSerializer- Consolidated VIF/VIFE/VIFE_FB/VIFE_FD into single
VifLookupServicewithFrozenDictionary - Async-first
MBusMasterwithCancellationTokenandIAsyncEnumerable<MeterInfo>scanning - TCP transport using
System.IO.Pipelinesfor frame boundary detection - UDP transport using raw
Socket - Serial transport wrapping
System.IO.PortswithPipeReader IServiceCollection.AddMBusCore()for DI registration- 168 unit tests using MSTest 4.x with
[DynamicData]against 80+ real meter hex files - Separate integration test project
- Upgraded to .NET 10 (from .NET Standard 2.0 / .NET Framework 4.6.1)
- Added GitHub Actions CI/CD workflows (build, test, NuGet publish)
- Fixed critical bug in
SelectSlave(InvalidCastException at runtime) - Fixed event handler memory leak in
MBusMaster - Implemented
SelectSlavesecondary address padding logic - Removed ~800 lines of dead/commented-out code
- Extracted
IValueInformationFieldinterface for VIF/VIFE types - Cached VIF/VIFE dictionary lookups for improved performance
- Extracted
MBusMastercommunication pattern into reusable helper - Refactored VIFE if/else chain to table-driven approach
- Extracted value parsing into dedicated
ValueParserclass - Consolidated magic numbers into
Constants.cs - Consolidated duplicate
LengthsInBitsTable - General code cleanup and modernization
- Serial communication capability
- Bug fixes
- Initial release
This project is licensed under the MIT License. See LICENSE for details.