Skip to content

jacobwi/SharpMeter

Repository files navigation

SharpMeter

A modern, high-performance .NET library for smart meter communication supporting both ANSI C12.18/C12.19 PSEM and DLMS/COSEM (IEC 62056) protocols.

CI NuGet License: MIT

Features

  • Dual Protocol — ANSI C12.18/C12.19 PSEM and DLMS/COSEM (IEC 62056)
  • Fully Async — All I/O uses async/await with CancellationToken
  • Zero-AllocationSpan<T>, ArrayPool<T>, System.IO.Pipelines on hot paths
  • Pluggable TransportITransport interface for serial, TCP, loopback, or custom
  • Table Deserialization — Three strategies: fluent builder, attribute mapping, JSON schema
  • Meter Emulator — Full PSEM emulator with configurable tables, state machine, and security
  • Result<T> — No exceptions for control flow; explicit error paths
  • IAsyncEnumerable — Stream large table reads as async enumerables
  • ILogger<T> — Microsoft.Extensions.Logging with source-generated delegates
  • Multi-Target — net9.0 and net10.0

Packages

Package Description
SharpMeter.Core Protocol types, CRC-16, Result<T>, enums, table definitions and deserializer
SharpMeter.Transport ITransport interface, serial, TCP, and loopback transports
SharpMeter.Client PSEM client — session, table, and procedure services
SharpMeter.Dlms DLMS/COSEM — HDLC framing, OBIS codes, A-XDR codec, GET/SET/ACTION
SharpMeter.Emulator Full PSEM meter emulator with in-memory table store

Installation

dotnet add package SharpMeter.Client      # PSEM (ANSI C12.18)
dotnet add package SharpMeter.Dlms        # DLMS/COSEM (IEC 62056)
dotnet add package SharpMeter.Emulator    # Testing

Quick Start — PSEM

using SharpMeter.Client;
using SharpMeter.Transport;
using Microsoft.Extensions.Logging.Abstractions;

var options = new TransportOptions { PortName = "COM3", BaudRate = 9600 };
await using var transport = new SerialTransport(options, NullLogger<SerialTransport>.Instance);
await using var client = new PsemClient(transport, NullLogger<PsemClient>.Instance);

var result = await client.ConnectAsync(userId: 2, userName: "SharpMeter", password: password);
if (result.IsSuccess)
{
    var table = await client.ReadTableAsync(1); // ST1: Manufacturer ID
    await client.DisconnectAsync();
}

Quick Start — Table Deserialization

Raw table bytes to named, typed fields — three ways:

using SharpMeter.Core.Tables;

// 1. Built-in definitions (ST0, ST1, ST3, ST5, ST8, ST52)
var registry = new TableRegistry();
var parsed = registry.Deserialize(1, tableData);
Console.WriteLine(parsed.Value.GetValue("MANUFACTURER")); // "GE"

// 2. Attribute mapping — strongly typed
[PsemTable(1, "Manufacturer ID", ExpectedSize = 32)]
public sealed record MfgTable
{
    [TableField(0, 4, FieldType.Ascii)]
    public string Manufacturer { get; init; } = "";
}
var typed = TableMapper.Deserialize<MfgTable>(tableData);

// 3. JSON schema — external table packs
registry.LoadFromJsonFile("tables/vendor-tables.json");

// 4. Fluent builder — inline definitions
registry.Register(TableDefinitionBuilder.Manufacturing(64, "Config")
    .WithSize(104)
    .UInt8("MODE").Ascii("SERIAL", 16)
    .Build());

Quick Start — DLMS/COSEM

using SharpMeter.Dlms;
using SharpMeter.Dlms.Enums;

var dlmsOptions = new DlmsClientOptions
{
    Authentication = AuthenticationMechanism.LowLevelSecurity,
    Password = "password"u8.ToArray()
};

await using var transport = new TcpTransport(
    new TransportOptions { Host = "192.168.1.100", Port = 4059 },
    NullLogger<TcpTransport>.Instance);
await using var client = new DlmsClient(transport, dlmsOptions, NullLogger<DlmsClient>.Instance);

await client.ConnectAsync();
var energy = await client.GetAttributeAsync(3, ObisCode.ActiveEnergyImportTotal, 2);
await client.DisconnectAsync();

Quick Start — Emulator

using SharpMeter.Client;
using SharpMeter.Emulator;

var emulator = new MeterEmulator(
    new EmulatorConfiguration { Manufacturer = "TEST", SerialNumber = "TEST0000000000001" },
    NullLogger<MeterEmulator>.Instance);
await using var transport = new EmulatorTransport(emulator, NullLogger<EmulatorTransport>.Instance);
await using var client = new PsemClient(transport, NullLogger<PsemClient>.Instance);

await client.ConnectAsync(password: null);
var table = await client.ReadTableAsync(1);
await client.DisconnectAsync();

Documentation

Full documentation is available at jacobwi.github.io/SharpMeter — built with MokaDocs.

Includes:

  • Getting Started guide
  • PSEM Protocol reference (session lifecycle, table reads, procedures)
  • Reading & Parsing Tables (ST vs MT, fluent builder, attribute mapping, JSON schema)
  • DLMS/COSEM guide (HDLC, OBIS, A-XDR, authentication)
  • Transport layer (serial, TCP, loopback, custom)
  • Meter emulator customization
  • Error handling with Result<T>
  • Binary serialization patterns
  • Full API reference (auto-generated)

Development

dotnet restore
dotnet build
dotnet test

# Serve documentation locally
dotnet tool install --global mokadocs
mokadocs serve

License

MIT