Meter Emulator

SharpMeter includes a full ANSI C12.18 PSEM meter emulator. It implements the complete session state machine and responds to all standard L7 services, allowing you to develop and test without physical hardware.

Basic Setup

using SharpMeter.Emulator;

var config = new EmulatorConfiguration
{
    Manufacturer = "TEST",
    ModelString = "EMULATOR",
    SerialNumber = "TESTMETER0000001",
    FirmwareVersion = [1, 0, 0, 0]
};

var emulator = new MeterEmulator(config, logger);
var transport = new EmulatorTransport(emulator, logger);

Configuration

var config = new EmulatorConfiguration
{
    // Identity
    Manufacturer = "ACME",
    ModelString = "SM-100",
    SerialNumber = "ACM0000000000001",
    FirmwareVersion = [2, 5, 0, 1],
    HardwareVersion = 3,

    // Security
    Password = new byte[20] { 0x01, 0x02, ... },
    ReadAccessLevel = AccessLevel.NoAccess,    // No auth needed to read
    WriteAccessLevel = AccessLevel.Reader,     // Reader+ to write
    ProcedureAccessLevel = AccessLevel.Master, // Master to execute

    // Meter features
    Target = MeterTarget.Electric,
    Model = MeterModel.Generic,
    Mode = MeterMode.DemandOnly,
    Upgrades = SoftswitchUpgrade.Tou | SoftswitchUpgrade.EventLog
};

State Machine

The emulator enforces the correct PSEM session sequence:

Idle → Identified → Negotiated → LoggedOn → Authenticated

Attempting services out of order returns InvalidServiceSequenceState.

Table Store

The emulator pre-populates standard tables (ST0, ST1, ST3, ST5, ST7, ST8, ST52, MT0) and supports dynamic table creation:

// Direct table manipulation for test setup
emulator.Tables.WriteTable(100, new byte[] { 0xAA, 0xBB, 0xCC });

// Check what tables exist
foreach (var id in emulator.Tables.TableIds)
    Console.WriteLine($"Table {id}");

Integration Testing

Use EmulatorTransport as a drop-in replacement for real hardware:

await using var transport = new EmulatorTransport(emulator, logger);
await using var client = new PsemClient(transport, logger);

// This is identical to real meter communication
var result = await client.ConnectAsync(password: null);
Assert.True(result.IsSuccess);

var table = await client.ReadTableAsync(1);
Assert.True(table.IsSuccess);

xUnit Test Example

public sealed class MeterTests : IAsyncDisposable
{
    private readonly EmulatorTransport _transport;
    private readonly PsemClient _client;

    public MeterTests()
    {
        var config = new EmulatorConfiguration { Manufacturer = "TEST" };
        var emulator = new MeterEmulator(config, NullLogger<MeterEmulator>.Instance);
        _transport = new EmulatorTransport(emulator, NullLogger<EmulatorTransport>.Instance);
        _client = new PsemClient(_transport, NullLogger<PsemClient>.Instance);
    }

    [Fact]
    public async Task Can_Read_ManufacturerId()
    {
        await _client.ConnectAsync(password: null);
        var result = await _client.ReadTableAsync(1);
        Assert.True(result.IsSuccess);
        Assert.True(result.Value.Length > 0);
    }

    public async ValueTask DisposeAsync()
    {
        await _client.DisposeAsync();
    }
}
Last updated: 2026-04-08
Was this page helpful?