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