Class
Sealed
public sealed class MeterEmulator
Namespace: SharpMeter.Emulator
Full ANSI C12.18 PSEM meter emulator that processes L7 service requests and generates responses.
Constructors
| Name | Description |
|---|---|
MeterEmulator(EmulatorConfiguration config, MeterEmulator> logger) |
Initializes a new MeterEmulator with the given configuration. |
MeterEmulator(EmulatorConfiguration config, MeterEmulator> logger)
MeterEmulator.MeterEmulator(EmulatorConfiguration config, ILogger<MeterEmulator> logger)
Initializes a new MeterEmulator with the given configuration.
Parameters
| Name | Type | Description |
|---|---|---|
config | SharpMeter.Emulator.EmulatorConfiguration | The emulator configuration. |
logger | ILogger<SharpMeter.Emulator.MeterEmulator> | The logger instance. |
Properties
| Name | Description |
|---|---|
CurrentAccessLevel |
Gets the current access level of the logged-on session. |
State |
Gets the current emulator state. |
Tables |
Gets the table store for direct manipulation in tests. |
CurrentAccessLevel
AccessLevel MeterEmulator.CurrentAccessLevel { get; set; }
Gets the current access level of the logged-on session.
State
EmulatorState MeterEmulator.State { get; set; }
Gets the current emulator state.
Tables
TableStore MeterEmulator.Tables { get; }
Gets the table store for direct manipulation in tests.
Methods
| Name | Description |
|---|---|
ProcessRequest(ReadOnlySpan<byte> payload) |
Processes an L7 PSEM service request and returns the L7 response payload. |
Reset() |
Resets the emulator to its initial state. |
ProcessRequest(ReadOnlySpan payload)
byte[] MeterEmulator.ProcessRequest(ReadOnlySpan<byte> payload)
Processes an L7 PSEM service request and returns the L7 response payload.
Parameters
| Name | Type | Description |
|---|---|---|
payload | ReadOnlySpan<byte> | The L7 request payload (service code + parameters). |
Returns: The L7 response payload.
Reset()
void MeterEmulator.Reset()
Resets the emulator to its initial state.
View Source
/// <summary>
/// Full ANSI C12.18 PSEM meter emulator that processes L7 service requests and generates responses.
/// </summary>
public sealed partial class MeterEmulator
{
#region Constructor
/// <summary>
/// Initializes a new <see cref = "MeterEmulator"/> with the given configuration.
/// </summary>
/// <param name = "config">The emulator configuration.</param>
/// <param name = "logger">The logger instance.</param>
public MeterEmulator(EmulatorConfiguration config, ILogger<MeterEmulator> logger)
{
_config = config;
Tables = new TableStore(config);
_logger = logger;
}
#endregion
#region Procedure Execution
private void ExecuteProcedure(byte[] procData)
{
if (procData.Length < 2)
return;
var procId = (ushort)((procData[0] << 8) | procData[1]);
var isManufacturer = procId >= PsemConstants.ManufacturingTableOffset;
var displayId = isManufacturer ? procId - PsemConstants.ManufacturingTableOffset : procId;
Log.ExecutingProcedure(_logger, isManufacturer ? "MP" : "SP", displayId);
// Build response in ST8
var st8 = new byte[16];
st8[0] = procData[0]; // Procedure ID high
st8[1] = procData[1]; // Procedure ID low
st8[2] = 0; // Sequence number
st8[3] = (byte)ProcedureResult.Completed; // Result: completed
Tables.WriteTable(PsemConstants.ProcedureResponseTable, st8);
}
#endregion
#region Log Messages
private static partial class Log
{
[LoggerMessage(Level = LogLevel.Debug, Message = "Processing service: {Service} (state={State})")]
public static partial void ProcessingService(ILogger logger, ServiceCode service, EmulatorState state);
[LoggerMessage(Level = LogLevel.Debug, Message = "Emulator state reset")]
public static partial void StateReset(ILogger logger);
[LoggerMessage(Level = LogLevel.Debug, Message = "{Service}: {Result}")]
public static partial void ServiceResult(ILogger logger, string service, string result);
[LoggerMessage(Level = LogLevel.Debug, Message = "Negotiate: OK (maxPacket={MaxPacket})")]
public static partial void NegotiateResult(ILogger logger, int maxPacket);
[LoggerMessage(Level = LogLevel.Debug, Message = "Logon: OK (user={User})")]
public static partial void LogonResult(ILogger logger, string user);
[LoggerMessage(Level = LogLevel.Warning, Message = "Security: password mismatch")]
public static partial void SecurityPasswordMismatch(ILogger logger);
[LoggerMessage(Level = LogLevel.Debug, Message = "Read ST/MT {TableId}: table not found")]
public static partial void TableNotFound(ILogger logger, ushort tableId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Read table {TableId}: {Length} bytes")]
public static partial void TableRead(ILogger logger, ushort tableId, int length);
[LoggerMessage(Level = LogLevel.Debug, Message = "Read offset table {TableId}: not found or invalid range")]
public static partial void TableOffsetNotFound(ILogger logger, ushort tableId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Read table {TableId} offset={Offset} count={Count}")]
public static partial void TableReadOffset(ILogger logger, ushort tableId, int offset, ushort count);
[LoggerMessage(Level = LogLevel.Debug, Message = "Write table {TableId}: {Length} bytes")]
public static partial void TableWrite(ILogger logger, ushort tableId, int length);
[LoggerMessage(Level = LogLevel.Debug, Message = "Write offset table {TableId}: failed")]
public static partial void TableWriteOffsetFailed(ILogger logger, ushort tableId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Write table {TableId} offset={Offset}: {Length} bytes")]
public static partial void TableWriteOffset(ILogger logger, ushort tableId, int offset, int length);
[LoggerMessage(Level = LogLevel.Debug, Message = "Executing {Type} procedure {Id}")]
public static partial void ExecutingProcedure(ILogger logger, string type, int id);
}
#endregion
#region Fields
private readonly EmulatorConfiguration _config;
private readonly ILogger<MeterEmulator> _logger;
private string _loggedOnUser = string.Empty;
private int _negotiatedMaxPacket;
#endregion
#region Properties
/// <summary>Gets the current emulator state.</summary>
public EmulatorState State { get; private set; } = EmulatorState.Idle;
/// <summary>Gets the table store for direct manipulation in tests.</summary>
public TableStore Tables { get; }
/// <summary>Gets the current access level of the logged-on session.</summary>
public AccessLevel CurrentAccessLevel { get; private set; } = AccessLevel.NoAccess;
#endregion
#region Public Methods
/// <summary>
/// Processes an L7 PSEM service request and returns the L7 response payload.
/// </summary>
/// <param name = "payload">The L7 request payload (service code + parameters).</param>
/// <returns>The L7 response payload.</returns>
public byte[] ProcessRequest(ReadOnlySpan<byte> payload)
{
if (payload.Length < 1)
return[(byte)ResponseCode.Error];
var serviceCode = (ServiceCode)payload[0];
ReadOnlySpan<byte> parameters = payload.Length > 1 ? payload[1..] : ReadOnlySpan<byte>.Empty;
Log.ProcessingService(_logger, serviceCode, State);
return serviceCode switch
{
ServiceCode.Identify => HandleIdentify(),
ServiceCode.Terminate => HandleTerminate(),
ServiceCode.Disconnect => HandleDisconnect(),
ServiceCode.ReadFull => HandleReadFull(parameters),
ServiceCode.ReadOffset => HandleReadOffset(parameters),
ServiceCode.WriteFull => HandleWriteFull(parameters),
ServiceCode.WriteOffset => HandleWriteOffset(parameters),
ServiceCode.Logon => HandleLogon(parameters),
ServiceCode.Security => HandleSecurity(parameters),
ServiceCode.Logoff => HandleLogoff(),
ServiceCode.Wait => HandleWait(),
_ when (byte)serviceCode >= 0x60 && (byte)serviceCode <= 0x6F => HandleNegotiate(parameters),
_ => [(byte)ResponseCode.ServiceNotSupported]
};
}
/// <summary>
/// Resets the emulator to its initial state.
/// </summary>
public void Reset()
{
State = EmulatorState.Idle;
CurrentAccessLevel = AccessLevel.NoAccess;
_loggedOnUser = string.Empty;
Log.StateReset(_logger);
}
#endregion
#region Service Handlers
private byte[] HandleIdentify()
{
if (State != EmulatorState.Idle)
return[(byte)ResponseCode.InvalidServiceSequenceState];
State = EmulatorState.Identified;
Log.ServiceResult(_logger, "Identify", "OK");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleNegotiate(ReadOnlySpan<byte> parameters)
{
if (State != EmulatorState.Identified)
return[(byte)ResponseCode.InvalidServiceSequenceState];
State = EmulatorState.Negotiated;
var maxPacket = parameters.Length >= 2 ? (parameters[0] << 8) | parameters[1] : 0;
_negotiatedMaxPacket = maxPacket;
Log.NegotiateResult(_logger, maxPacket);
return[(byte)ResponseCode.Ok];
}
private byte[] HandleLogon(ReadOnlySpan<byte> parameters)
{
if (State is not (EmulatorState.Negotiated or EmulatorState.Identified))
return[(byte)ResponseCode.InvalidServiceSequenceState];
if (parameters.Length >= 12)
{
var userName = Encoding.ASCII.GetString(parameters.Slice(2, 10)).TrimEnd('\0');
_loggedOnUser = userName;
CurrentAccessLevel = AccessLevel.NoAccess;
}
State = EmulatorState.LoggedOn;
Log.LogonResult(_logger, _loggedOnUser);
return[(byte)ResponseCode.Ok];
}
private byte[] HandleSecurity(ReadOnlySpan<byte> parameters)
{
if (State != EmulatorState.LoggedOn)
return[(byte)ResponseCode.InvalidServiceSequenceState];
if (_config.Password.Length > 0)
{
var expected = new byte[20];
_config.Password.AsSpan(0, Math.Min(_config.Password.Length, 20)).CopyTo(expected);
var provided = new byte[20];
if (parameters.Length > 0)
parameters[..Math.Min(parameters.Length, 20)].CopyTo(provided);
if (!expected.AsSpan().SequenceEqual(provided))
{
Log.SecurityPasswordMismatch(_logger);
return[(byte)ResponseCode.InsufficientSecurityClearance];
}
}
CurrentAccessLevel = AccessLevel.Master;
State = EmulatorState.Authenticated;
Log.ServiceResult(_logger, "Security", "OK (access=Master)");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleLogoff()
{
CurrentAccessLevel = AccessLevel.NoAccess;
_loggedOnUser = string.Empty;
State = EmulatorState.Identified;
Log.ServiceResult(_logger, "Logoff", "OK");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleTerminate()
{
State = EmulatorState.Idle;
CurrentAccessLevel = AccessLevel.NoAccess;
_loggedOnUser = string.Empty;
Log.ServiceResult(_logger, "Terminate", "OK");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleDisconnect()
{
State = EmulatorState.Idle;
CurrentAccessLevel = AccessLevel.NoAccess;
_loggedOnUser = string.Empty;
Log.ServiceResult(_logger, "Disconnect", "OK");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleWait()
{
Log.ServiceResult(_logger, "Wait", "OK");
return[(byte)ResponseCode.Ok];
}
private byte[] HandleReadFull(ReadOnlySpan<byte> parameters)
{
if (CurrentAccessLevel < _config.ReadAccessLevel)
return[(byte)ResponseCode.InsufficientSecurityClearance];
if (parameters.Length < 2)
return[(byte)ResponseCode.InappropriateAction];
var tableId = (ushort)((parameters[0] << 8) | parameters[1]);
var data = Tables.ReadTable(tableId);
if (data is null)
{
Log.TableNotFound(_logger, tableId);
return[(byte)ResponseCode.OperationNotPossible];
}
// Response: OK + count(2 bytes) + data + checksum(1 byte)
var response = new byte[1 + 2 + data.Length + 1];
response[0] = (byte)ResponseCode.Ok;
response[1] = (byte)(data.Length >> 8);
response[2] = (byte)(data.Length & 0xFF);
data.CopyTo(response, 3);
// Simple checksum (sum of data bytes)
byte checksum = 0;
foreach (var b in data)
checksum += b;
response[^1] = (byte)(~checksum + 1);
Log.TableRead(_logger, tableId, data.Length);
return response;
}
private byte[] HandleReadOffset(ReadOnlySpan<byte> parameters)
{
if (CurrentAccessLevel < _config.ReadAccessLevel)
return[(byte)ResponseCode.InsufficientSecurityClearance];
if (parameters.Length < 7)
return[(byte)ResponseCode.InappropriateAction];
var tableId = (ushort)((parameters[0] << 8) | parameters[1]);
var offset = (parameters[2] << 16) | (parameters[3] << 8) | parameters[4];
var count = (ushort)((parameters[5] << 8) | parameters[6]);
var data = Tables.ReadTableOffset(tableId, offset, count);
if (data is null)
{
Log.TableOffsetNotFound(_logger, tableId);
return[(byte)ResponseCode.OperationNotPossible];
}
var response = new byte[1 + 2 + data.Length + 1];
response[0] = (byte)ResponseCode.Ok;
response[1] = (byte)(data.Length >> 8);
response[2] = (byte)(data.Length & 0xFF);
data.CopyTo(response, 3);
byte checksum = 0;
foreach (var b in data)
checksum += b;
response[^1] = (byte)(~checksum + 1);
Log.TableReadOffset(_logger, tableId, offset, count);
return response;
}
private byte[] HandleWriteFull(ReadOnlySpan<byte> parameters)
{
if (CurrentAccessLevel < _config.WriteAccessLevel)
return[(byte)ResponseCode.InsufficientSecurityClearance];
if (parameters.Length < 2)
return[(byte)ResponseCode.InappropriateAction];
var tableId = (ushort)((parameters[0] << 8) | parameters[1]);
var data = parameters[2..].ToArray();
Tables.WriteTable(tableId, data);
Log.TableWrite(_logger, tableId, data.Length);
// Handle procedure initiation (ST7 write triggers procedure execution)
if (tableId == PsemConstants.ProcedureInitiationTable)
ExecuteProcedure(data);
return[(byte)ResponseCode.Ok];
}
private byte[] HandleWriteOffset(ReadOnlySpan<byte> parameters)
{
if (CurrentAccessLevel < _config.WriteAccessLevel)
return[(byte)ResponseCode.InsufficientSecurityClearance];
if (parameters.Length < 5)
return[(byte)ResponseCode.InappropriateAction];
var tableId = (ushort)((parameters[0] << 8) | parameters[1]);
var offset = (parameters[2] << 16) | (parameters[3] << 8) | parameters[4];
ReadOnlySpan<byte> data = parameters[5..];
if (!Tables.WriteTableOffset(tableId, offset, data))
{
Log.TableWriteOffsetFailed(_logger, tableId);
return[(byte)ResponseCode.OperationNotPossible];
}
Log.TableWriteOffset(_logger, tableId, offset, data.Length);
return[(byte)ResponseCode.Ok];
}
#endregion
}