Class
Sealed
public sealed class EmulatorTransport : ITransport
Namespace: SharpMeter.Emulator
A transport implementation that emulates a PSEM meter. Incoming frames are processed by the emulator and responses are generated automatically. This transport can be used with Client.PsemClient for testing.
Inheritance
Inherits from: ITransport
Constructors
| Name | Description |
|---|---|
EmulatorTransport(MeterEmulator emulator, EmulatorTransport> logger) |
Initializes a new EmulatorTransport wrapping the given emulator. |
EmulatorTransport(MeterEmulator emulator, EmulatorTransport> logger)
EmulatorTransport.EmulatorTransport(MeterEmulator emulator, ILogger<EmulatorTransport> logger)
Initializes a new EmulatorTransport wrapping the given emulator.
Parameters
| Name | Type | Description |
|---|---|---|
emulator | SharpMeter.Emulator.MeterEmulator | The meter emulator instance. |
logger | ILogger<SharpMeter.Emulator.EmulatorTransport> | The logger instance. |
Properties
| Name | Description |
|---|---|
IsConnected |
Methods
Type Relationships
classDiagram
style EmulatorTransport fill:#f9f,stroke:#333,stroke-width:2px
EmulatorTransport --|> ITransport : inherits
View Source
/// <summary>
/// A transport implementation that emulates a PSEM meter.
/// Incoming frames are processed by the emulator and responses are generated automatically.
/// This transport can be used with <see cref = "SharpMeter.Client.PsemClient"/> for testing.
/// </summary>
public sealed partial class EmulatorTransport : ITransport
{
#region Constructor
/// <summary>
/// Initializes a new <see cref = "EmulatorTransport"/> wrapping the given emulator.
/// </summary>
/// <param name = "emulator">The meter emulator instance.</param>
/// <param name = "logger">The logger instance.</param>
public EmulatorTransport(MeterEmulator emulator, ILogger<EmulatorTransport> logger)
{
_emulator = emulator;
_logger = logger;
}
#endregion
#region IAsyncDisposable
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
await _responsePipe.Writer.CompleteAsync();
await _responsePipe.Reader.CompleteAsync();
await DisconnectAsync();
}
#endregion
#region Log Messages
private static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Emulator transport connected")]
public static partial void TransportConnected(ILogger logger);
[LoggerMessage(Level = LogLevel.Information, Message = "Emulator transport disconnected")]
public static partial void TransportDisconnected(ILogger logger);
[LoggerMessage(Level = LogLevel.Debug, Message = "Emulator RX [{Length} bytes]: {Data}")]
public static partial void EmulatorReceived(ILogger logger, int length, string data);
[LoggerMessage(Level = LogLevel.Debug, Message = "Emulator TX [{Length} bytes]: {Data}")]
public static partial void EmulatorTransmitted(ILogger logger, int length, string data);
[LoggerMessage(Level = LogLevel.Debug, Message = "Emulator received control byte: 0x{Byte:X2}")]
public static partial void ControlByteReceived(ILogger logger, byte @byte);
}
#endregion
#region Fields
private readonly MeterEmulator _emulator;
private readonly ILogger<EmulatorTransport> _logger;
private readonly Pipe _responsePipe = new();
#endregion
#region ITransport Members
/// <inheritdoc/>
public bool IsConnected { get; private set; }
/// <inheritdoc/>
public ValueTask<Result<bool>> ConnectAsync(CancellationToken cancellationToken = default)
{
IsConnected = true;
_emulator.Reset();
Log.TransportConnected(_logger);
return ValueTask.FromResult<Result<bool>>(true);
}
/// <inheritdoc/>
public ValueTask DisconnectAsync(CancellationToken cancellationToken = default)
{
IsConnected = false;
Log.TransportDisconnected(_logger);
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public async ValueTask<Result<bool>> SendAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
if (!IsConnected)
return PsemError.Transport("Emulator transport is not connected");
if (_logger.IsEnabled(LogLevel.Debug))
{
var hex = Convert.ToHexString(data.Span);
Log.EmulatorReceived(_logger, data.Length, hex);
}
// Decode the incoming frame
Result<PsemFrame> frameResult = PsemFrame.Decode(data.Span);
if (frameResult.IsFailure)
{
// Send NAK response
await WriteResponse([PsemFrame.Nak], cancellationToken);
return true;
}
// Process the frame through the emulator
var response = _emulator.ProcessRequest(frameResult.Value.Data.Span);
// Build response frame
var responseFrame = BuildResponseFrame(response);
await WriteResponse(responseFrame, cancellationToken);
return true;
}
/// <inheritdoc/>
public async ValueTask<Result<int>> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (!IsConnected)
return PsemError.Transport("Emulator transport is not connected");
try
{
ReadResult readResult = await _responsePipe.Reader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> sequence = readResult.Buffer;
if (sequence.IsEmpty && readResult.IsCompleted)
return 0;
var bytesToCopy = (int)Math.Min(sequence.Length, buffer.Length);
sequence.Slice(0, bytesToCopy).CopyTo(buffer.Span);
_responsePipe.Reader.AdvanceTo(sequence.GetPosition(bytesToCopy));
if (_logger.IsEnabled(LogLevel.Debug))
{
var hex = Convert.ToHexString(buffer.Span[..bytesToCopy]);
Log.EmulatorTransmitted(_logger, bytesToCopy, hex);
}
return bytesToCopy;
}
catch (OperationCanceledException)
{
return PsemError.Timeout("emulator receive");
}
}
/// <inheritdoc/>
public ValueTask SendControlAsync(byte controlByte, CancellationToken cancellationToken = default)
{
// Emulator ignores ACK/NAK/CAN from client side
Log.ControlByteReceived(_logger, controlByte);
return ValueTask.CompletedTask;
}
#endregion
#region Private Methods
private static byte[] BuildResponseFrame(ReadOnlySpan<byte> payload)
{
var frame = new PsemFrame
{
Control = new ControlByte(ControlFlags.SinglePacket, 0),
Sequence = 0,
Data = payload.ToArray()
};
var buffer = new byte[frame.TotalLength];
frame.Encode(buffer);
return buffer;
}
private async ValueTask WriteResponse(byte[] data, CancellationToken cancellationToken)
{
Memory<byte> memory = _responsePipe.Writer.GetMemory(data.Length);
data.CopyTo(memory);
_responsePipe.Writer.Advance(data.Length);
await _responsePipe.Writer.FlushAsync(cancellationToken);
}
#endregion
}