public sealed class PsemClient : IAsyncDisposable
Namespace: SharpMeter.Client
Inheritance
Inherits from: IAsyncDisposable
Constructors
| Name | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
transport | ITransport | The transport layer implementation. |
logger | ILogger<SharpMeter.Client.PsemClient> | The logger instance. |
Properties
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
userId | ushort | The user ID for logon. |
userName | string | The username for logon (max 10 chars). |
password | ReadOnlyMemory<byte>? | The security password bytes (max 20 bytes). |
baudRates | BaudRate[]? | Optional baud rates to negotiate. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
procedureId | ushort | The manufacturer procedure ID (0-2047, offset added automatically). |
parameters | ReadOnlyMemory<byte> | The procedure parameters. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
procedureId | ushort | The standard procedure ID (0-2047). |
parameters | ReadOnlyMemory<byte> | The procedure parameters. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
tableId | ushort | The table ID to read. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
tableId | ushort | The table ID to read. |
offset | int | Byte offset within the table. |
count | ushort | Number of bytes to read. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
tableId | ushort | The table ID to read. |
chunkSize | ushort | The size of each read chunk in bytes. |
totalSize | int | The total table size in bytes. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
tableId | ushort | The table ID to write. |
data | ReadOnlyMemory<byte> | The data to write. |
cancellationToken | CancellationToken | Cancellation 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
| Name | Type | Description |
|---|---|---|
tableId | ushort | The table ID to write. |
offset | int | The byte offset within the table. |
data | ReadOnlyMemory<byte> | The data to write. |
cancellationToken | CancellationToken | Cancellation 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
}