Class Sealed
public sealed class PsemClient : IAsyncDisposable

Namespace: SharpMeter.Client

High-level PSEM client for communicating with ANSI C12.18/C12.19 electric meters. Provides session management, table read/write, and procedure execution.

Inheritance

Inherits from: IAsyncDisposable

Constructors

NameDescription
PsemClient(ITransport transport, PsemClient> logger) Initializes a new instance of PsemClient.

PsemClient(ITransport transport, PsemClient> logger)

PsemClient.PsemClient(ITransport transport, ILogger<PsemClient> logger)

Initializes a new instance of PsemClient.

Parameters

NameTypeDescription
transportITransportThe transport layer implementation.
loggerILogger<SharpMeter.Client.PsemClient>The logger instance.

Properties

NameDescription
IsSessionActive Gets whether a PSEM session is currently active.
MeterInfo Gets the meter information populated after a successful connection.

IsSessionActive

bool PsemClient.IsSessionActive { get; set; }

Gets whether a PSEM session is currently active.

MeterInfo

MeterInfo PsemClient.MeterInfo { get; }

Gets the meter information populated after a successful connection.

Methods

NameDescription
ConnectAsync(…) Establishes a full PSEM session: connect transport, identify, negotiate, logon, and authenticate.
DisconnectAsync(CancellationToken cancellationToken) Gracefully terminates the PSEM session: logoff, terminate, disconnect.
DisposeAsync()
ExecuteManufacturerProcedureAsync(…) Executes a manufacturer procedure (MP).
ExecuteStandardProcedureAsync(…) Executes a standard procedure (SP).
ReadTableAsync(ushort tableId, CancellationToken cancellationToken) Reads a complete table from the meter.
ReadTableOffsetAsync(…) Reads a partial (offset) table from the meter.
StreamTableAsync(…) Streams a large table read as an async enumerable of chunks. Each yielded TableData contains one packet's worth of data.
WaitAsync(CancellationToken cancellationToken) Sends a keep-alive (Wait) to prevent session timeout.
WriteTableAsync(…) Writes data to a complete table.
WriteTableOffsetAsync(…) Writes data at a specific offset within a table.

ConnectAsync(ushort userId, string userName, ReadOnlyMemory? password, BaudRate[]? baudRates, CancellationToken cancellationToken)

ValueTask<Result<bool>> PsemClient.ConnectAsync(ushort userId = 2, string userName = "SharpMeter", ReadOnlyMemory<byte>? password = null, BaudRate[]? baudRates = null, CancellationToken cancellationToken = null)

Establishes a full PSEM session: connect transport, identify, negotiate, logon, and authenticate.

Parameters

NameTypeDescription
userIdushortThe user ID for logon.
userNamestringThe username for logon (max 10 chars).
passwordReadOnlyMemory<byte>?The security password bytes (max 20 bytes).
baudRatesBaudRate[]?Optional baud rates to negotiate.
cancellationTokenCancellationTokenCancellation token.

Returns: A result indicating success or the first error encountered.

DisconnectAsync(CancellationToken cancellationToken)

ValueTask PsemClient.DisconnectAsync(CancellationToken cancellationToken = null)

Gracefully terminates the PSEM session: logoff, terminate, disconnect.

Parameters

NameTypeDescription
cancellationTokenCancellationTokenCancellation token.

ExecuteManufacturerProcedureAsync(ushort procedureId, ReadOnlyMemory parameters, CancellationToken cancellationToken)

ValueTask<Result<ProcedureResult>> PsemClient.ExecuteManufacturerProcedureAsync(ushort procedureId, ReadOnlyMemory<byte> parameters = null, CancellationToken cancellationToken = null)

Executes a manufacturer procedure (MP).

Parameters

NameTypeDescription
procedureIdushortThe manufacturer procedure ID (0-2047, offset added automatically).
parametersReadOnlyMemory<byte>The procedure parameters.
cancellationTokenCancellationTokenCancellation token.

Returns: The procedure result code.

ExecuteStandardProcedureAsync(ushort procedureId, ReadOnlyMemory parameters, CancellationToken cancellationToken)

ValueTask<Result<ProcedureResult>> PsemClient.ExecuteStandardProcedureAsync(ushort procedureId, ReadOnlyMemory<byte> parameters = null, CancellationToken cancellationToken = null)

Executes a standard procedure (SP).

Parameters

NameTypeDescription
procedureIdushortThe standard procedure ID (0-2047).
parametersReadOnlyMemory<byte>The procedure parameters.
cancellationTokenCancellationTokenCancellation token.

Returns: The procedure result code.

ReadTableAsync(ushort tableId, CancellationToken cancellationToken)

ValueTask<Result<TableData>> PsemClient.ReadTableAsync(ushort tableId, CancellationToken cancellationToken = null)

Reads a complete table from the meter.

Parameters

NameTypeDescription
tableIdushortThe table ID to read.
cancellationTokenCancellationTokenCancellation token.

Returns: The table data, or an error.

ReadTableOffsetAsync(ushort tableId, int offset, ushort count, CancellationToken cancellationToken)

ValueTask<Result<TableData>> PsemClient.ReadTableOffsetAsync(ushort tableId, int offset, ushort count, CancellationToken cancellationToken = null)

Reads a partial (offset) table from the meter.

Parameters

NameTypeDescription
tableIdushortThe table ID to read.
offsetintByte offset within the table.
countushortNumber of bytes to read.
cancellationTokenCancellationTokenCancellation token.

Returns: The partial table data, or an error.

StreamTableAsync(ushort tableId, ushort chunkSize, int totalSize, CancellationToken cancellationToken)

IAsyncEnumerable<Result<TableData>> PsemClient.StreamTableAsync(ushort tableId, ushort chunkSize, int totalSize, CancellationToken cancellationToken = null)

Streams a large table read as an async enumerable of chunks. Each yielded TableData contains one packet's worth of data.

Parameters

NameTypeDescription
tableIdushortThe table ID to read.
chunkSizeushortThe size of each read chunk in bytes.
totalSizeintThe total table size in bytes.
cancellationTokenCancellationTokenCancellation token.

Returns: An async enumerable of table data chunks.

WaitAsync(CancellationToken cancellationToken)

ValueTask<Result<bool>> PsemClient.WaitAsync(CancellationToken cancellationToken = null)

Sends a keep-alive (Wait) to prevent session timeout.

Parameters

NameTypeDescription
cancellationTokenCancellationTokenCancellation token.

Returns: A result indicating success or error.

WriteTableAsync(ushort tableId, ReadOnlyMemory data, CancellationToken cancellationToken)

ValueTask<Result<bool>> PsemClient.WriteTableAsync(ushort tableId, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = null)

Writes data to a complete table.

Parameters

NameTypeDescription
tableIdushortThe table ID to write.
dataReadOnlyMemory<byte>The data to write.
cancellationTokenCancellationTokenCancellation token.

Returns: A result indicating success or error.

WriteTableOffsetAsync(ushort tableId, int offset, ReadOnlyMemory data, CancellationToken cancellationToken)

ValueTask<Result<bool>> PsemClient.WriteTableOffsetAsync(ushort tableId, int offset, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = null)

Writes data at a specific offset within a table.

Parameters

NameTypeDescription
tableIdushortThe table ID to write.
offsetintThe byte offset within the table.
dataReadOnlyMemory<byte>The data to write.
cancellationTokenCancellationTokenCancellation token.

Returns: A result indicating success or error.

Type Relationships
classDiagram
                    style PsemClient fill:#f9f,stroke:#333,stroke-width:2px
                    PsemClient --|> IAsyncDisposable : inherits
                
View Source
/// <summary>
///     High-level PSEM client for communicating with ANSI C12.18/C12.19 electric meters.
///     Provides session management, table read/write, and procedure execution.
/// </summary>
public sealed partial class PsemClient : IAsyncDisposable
{
#region Constructor
    /// <summary>
    ///     Initializes a new instance of <see cref = "PsemClient"/>.
    /// </summary>
    /// <param name = "transport">The transport layer implementation.</param>
    /// <param name = "logger">The logger instance.</param>
    public PsemClient(ITransport transport, ILogger<PsemClient> logger)
    {
        _transport = transport;
        _logger = logger;
        _codec = new FrameCodec(logger);
        _session = new SessionService(_transport, _codec, logger);
        _tables = new TableService(_transport, _codec, logger);
        _procedures = new ProcedureService(_tables, logger);
    }

#endregion
#region IAsyncDisposable
    /// <inheritdoc/>
    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
        await _transport.DisposeAsync();
    }

#endregion
#region Log Messages
    private static partial class Log
    {
        [LoggerMessage(Level = LogLevel.Information, Message = "Starting PSEM session...")]
        public static partial void StartingSession(ILogger logger);
        [LoggerMessage(Level = LogLevel.Information, Message = "PSEM session established successfully")]
        public static partial void SessionEstablished(ILogger logger);
        [LoggerMessage(Level = LogLevel.Information, Message = "PSEM session terminated")]
        public static partial void SessionTerminated(ILogger logger);
    }

#endregion
#region Fields
    private readonly ITransport _transport;
    private readonly ILogger<PsemClient> _logger;
    private readonly FrameCodec _codec;
    private readonly SessionService _session;
    private readonly TableService _tables;
    private readonly ProcedureService _procedures;
#endregion
#region Properties
    /// <summary>Gets the meter information populated after a successful connection.</summary>
    public MeterInfo MeterInfo { get; } = new();
    /// <summary>Gets whether a PSEM session is currently active.</summary>
    public bool IsSessionActive { get; private set; }

#endregion
#region Session Management
    /// <summary>
    ///     Establishes a full PSEM session: connect transport, identify, negotiate, logon, and authenticate.
    /// </summary>
    /// <param name = "userId">The user ID for logon.</param>
    /// <param name = "userName">The username for logon (max 10 chars).</param>
    /// <param name = "password">The security password bytes (max 20 bytes).</param>
    /// <param name = "baudRates">Optional baud rates to negotiate.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>A result indicating success or the first error encountered.</returns>
    public async ValueTask<Result<bool>> ConnectAsync(ushort userId = 2, string userName = "SharpMeter", ReadOnlyMemory<byte>? password = null, BaudRate[]? baudRates = null, CancellationToken cancellationToken = default)
    {
        Log.StartingSession(_logger);
        _codec.Reset();
        // Connect transport
        Result<bool> connectResult = await _transport.ConnectAsync(cancellationToken);
        if (connectResult.IsFailure)
            return connectResult.Error;
        // Identify
        Result<bool> identifyResult = await _session.IdentifyAsync(cancellationToken);
        if (identifyResult.IsFailure)
            return identifyResult.Error;
        // Negotiate
        baudRates ??= [BaudRate.Baud9600, BaudRate.Baud14400, BaudRate.Baud19200, BaudRate.Baud28800, BaudRate.Baud38400];
        Result<bool> negotiateResult = await _session.NegotiateAsync(PsemConstants.MaxPacketLength, PsemConstants.MaxNumberOfPackets, baudRates, cancellationToken);
        if (negotiateResult.IsFailure)
            return negotiateResult.Error;
        // Logon
        Result<bool> logonResult = await _session.LogonAsync(userId, userName, cancellationToken);
        if (logonResult.IsFailure)
            return logonResult.Error;
        // Security (if password provided)
        if (password is { Length: > 0 } pwd)
        {
            Result<bool> securityResult = await _session.SecurityAsync(pwd, cancellationToken);
            if (securityResult.IsFailure)
                return securityResult.Error;
        }

        IsSessionActive = true;
        Log.SessionEstablished(_logger);
        return true;
    }

    /// <summary>
    ///     Gracefully terminates the PSEM session: logoff, terminate, disconnect.
    /// </summary>
    /// <param name = "cancellationToken">Cancellation token.</param>
    public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default)
    {
        if (IsSessionActive)
        {
            await _session.LogoffAsync(cancellationToken);
            await _session.TerminateAsync(cancellationToken);
            IsSessionActive = false;
        }

        await _transport.DisconnectAsync(cancellationToken);
        Log.SessionTerminated(_logger);
    }

#endregion
#region Table Operations
    /// <summary>
    ///     Reads a complete table from the meter.
    /// </summary>
    /// <param name = "tableId">The table ID to read.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>The table data, or an error.</returns>
    public ValueTask<Result<TableData>> ReadTableAsync(ushort tableId, CancellationToken cancellationToken = default) => _tables.ReadFullAsync(tableId, cancellationToken);
    /// <summary>
    ///     Reads a partial (offset) table from the meter.
    /// </summary>
    /// <param name = "tableId">The table ID to read.</param>
    /// <param name = "offset">Byte offset within the table.</param>
    /// <param name = "count">Number of bytes to read.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>The partial table data, or an error.</returns>
    public ValueTask<Result<TableData>> ReadTableOffsetAsync(ushort tableId, int offset, ushort count, CancellationToken cancellationToken = default) => _tables.ReadOffsetAsync(tableId, offset, count, cancellationToken);
    /// <summary>
    ///     Streams a large table read as an async enumerable of chunks.
    ///     Each yielded <see cref = "TableData"/> contains one packet's worth of data.
    /// </summary>
    /// <param name = "tableId">The table ID to read.</param>
    /// <param name = "chunkSize">The size of each read chunk in bytes.</param>
    /// <param name = "totalSize">The total table size in bytes.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>An async enumerable of table data chunks.</returns>
    public IAsyncEnumerable<Result<TableData>> StreamTableAsync(ushort tableId, ushort chunkSize, int totalSize, CancellationToken cancellationToken = default) => _tables.StreamReadAsync(tableId, chunkSize, totalSize, cancellationToken);
    /// <summary>
    ///     Writes data to a complete table.
    /// </summary>
    /// <param name = "tableId">The table ID to write.</param>
    /// <param name = "data">The data to write.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>A result indicating success or error.</returns>
    public ValueTask<Result<bool>> WriteTableAsync(ushort tableId, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default) => _tables.WriteFullAsync(tableId, data, cancellationToken);
    /// <summary>
    ///     Writes data at a specific offset within a table.
    /// </summary>
    /// <param name = "tableId">The table ID to write.</param>
    /// <param name = "offset">The byte offset within the table.</param>
    /// <param name = "data">The data to write.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>A result indicating success or error.</returns>
    public ValueTask<Result<bool>> WriteTableOffsetAsync(ushort tableId, int offset, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default) => _tables.WriteOffsetAsync(tableId, offset, data, cancellationToken);
#endregion
#region Procedure Operations
    /// <summary>
    ///     Executes a standard procedure (SP).
    /// </summary>
    /// <param name = "procedureId">The standard procedure ID (0-2047).</param>
    /// <param name = "parameters">The procedure parameters.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>The procedure result code.</returns>
    public ValueTask<Result<ProcedureResult>> ExecuteStandardProcedureAsync(ushort procedureId, ReadOnlyMemory<byte> parameters = default, CancellationToken cancellationToken = default) => _procedures.ExecuteAsync(procedureId, false, parameters, cancellationToken);
    /// <summary>
    ///     Executes a manufacturer procedure (MP).
    /// </summary>
    /// <param name = "procedureId">The manufacturer procedure ID (0-2047, offset added automatically).</param>
    /// <param name = "parameters">The procedure parameters.</param>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>The procedure result code.</returns>
    public ValueTask<Result<ProcedureResult>> ExecuteManufacturerProcedureAsync(ushort procedureId, ReadOnlyMemory<byte> parameters = default, CancellationToken cancellationToken = default) => _procedures.ExecuteAsync(procedureId, true, parameters, cancellationToken);
    /// <summary>
    ///     Sends a keep-alive (Wait) to prevent session timeout.
    /// </summary>
    /// <param name = "cancellationToken">Cancellation token.</param>
    /// <returns>A result indicating success or error.</returns>
    public ValueTask<Result<bool>> WaitAsync(CancellationToken cancellationToken = default) => _session.WaitAsync(cancellationToken);
#endregion
}
Was this page helpful?