Emulator Customization

The SharpMeter emulator supports custom table data, procedure behavior, and full state inspection for advanced testing scenarios.

Custom Table Data

Pre-populating Tables

Use TableStore to seed tables before connecting:

var emulator = new MeterEmulator(config, logger);

// Add a custom manufacturing table (MT64)
var mt64Data = new byte[104];
mt64Data[0] = 0x02; // Equation: 2-Element 3W
mt64Data[1] = 0x01; // Line frequency: 60 Hz
emulator.Tables.WriteTable(2112, mt64Data); // MT64 = 2048 + 64

// Override a standard table
var customSt5 = System.Text.Encoding.ASCII.GetBytes("CUSTOM_SERIAL_00001\0");
emulator.Tables.WriteTable(5, customSt5);

Dynamic Tables

Tables can be added or modified at any point — even mid-session:

await client.ConnectAsync(password: null);

// Simulate a meter event by modifying ST3 (status flags)
var st3 = emulator.Tables.ReadTable(3) ?? new byte[5];
st3[1] |= 0x01; // Set UNPROGRAMMED_FLAG
emulator.Tables.WriteTable(3, st3);

// Now the client reads the updated status
var result = await client.ReadTableAsync(3);

Inspecting Tables After Writes

Verify what the client wrote:

await client.WriteTableAsync(100, new byte[] { 0xAA, 0xBB, 0xCC });

// Inspect from the emulator side
var written = emulator.Tables.ReadTable(100);
Assert.Equal(new byte[] { 0xAA, 0xBB, 0xCC }, written);

Listing All Tables

foreach (var tableId in emulator.Tables.TableIds)
{
    var data = emulator.Tables.ReadTable(tableId);
    var prefix = tableId >= 2048 ? $"MT{tableId - 2048}" : $"ST{tableId}";
    Console.WriteLine($"{prefix}: {data?.Length ?? 0} bytes");
}

Partial Table Reads

The emulator supports offset reads for testing chunked/streaming access:

// Write a 256-byte table
var bigTable = new byte[256];
for (int i = 0; i < 256; i++) bigTable[i] = (byte)i;
emulator.Tables.WriteTable(200, bigTable);

// Read in chunks via the client
await foreach (var chunk in client.StreamTableAsync(200, chunkSize: 64, totalSize: 256))
{
    Assert.True(chunk.IsSuccess);
    // First chunk: bytes 0-63
    // Second chunk: bytes 64-127
    // etc.
}

Procedure Handling

All procedures written to ST7 are automatically "completed" with ProcedureResult.Completed in ST8. You can inspect what was executed:

// Execute a standard procedure
await client.ExecuteStandardProcedureAsync(0); // Cold Start

// Check what was written to ST7
var st7 = emulator.Tables.ReadTable(7);
// st7[0..1] = procedure ID

// Check the response in ST8
var st8 = emulator.Tables.ReadTable(8);
// st8[3] = ProcedureResult.Completed (0x00)

State Machine Inspection

Monitor the emulator state for test assertions:

Assert.Equal(EmulatorState.Idle, emulator.State);

await client.ConnectAsync(password: null);
Assert.Equal(EmulatorState.Authenticated, emulator.State);
Assert.Equal(AccessLevel.Master, emulator.CurrentAccessLevel);

await client.DisconnectAsync();
Assert.Equal(EmulatorState.Idle, emulator.State);

Security Testing

Password Enforcement

var config = new EmulatorConfiguration
{
    Password = new byte[] { 0x01, 0x02, 0x03, 0, 0, 0, 0, 0, 0, 0,
                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    ReadAccessLevel = AccessLevel.Reader,
    WriteAccessLevel = AccessLevel.Master,
    ProcedureAccessLevel = AccessLevel.Master
};

var emulator = new MeterEmulator(config, logger);

Access Level Testing

// Connect without password — gets NoAccess after logon
var result = await client.ConnectAsync(password: null);

// Reads work if ReadAccessLevel is NoAccess
var table = await client.ReadTableAsync(1);
Assert.True(table.IsSuccess);

// Writes fail if WriteAccessLevel requires Master
var writeResult = await client.WriteTableAsync(100, new byte[] { 0x01 });
Assert.True(writeResult.IsFailure);

Pre-populated Standard Tables

The emulator initializes these tables from EmulatorConfiguration:

Table Name Source
ST0 General Configuration Target field from config
ST1 Manufacturer ID Manufacturer, ModelString, SerialNumber, FirmwareVersion
ST3 Mode/Status All zeros (normal operation)
ST5 Device Identification SerialNumber (20 chars)
ST7 Procedure Initiation Empty (64 bytes, write target)
ST8 Procedure Response Empty (16 bytes, read after procedure)
ST52 Clock Current UTC time
MT0 Device Table Model, Mode, Upgrades bitfield
Last updated: 2026-04-08
Was this page helpful?