Client.PsemClient for testing." /> Client.PsemClient for testing." /> Client.PsemClient for testing." />
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

NameDescription
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

NameTypeDescription
emulatorSharpMeter.Emulator.MeterEmulatorThe meter emulator instance.
loggerILogger<SharpMeter.Emulator.EmulatorTransport>The logger instance.

Properties

NameDescription
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
}
Was this page helpful?