public sealed class DlmsClient : IAsyncDisposable
Namespace: SharpMeter.Dlms
Inheritance
Inherits from: IAsyncDisposable
Constructors
| Name | Description |
|---|---|
DlmsClient(…) |
Initializes a new instance of DlmsClient. |
DlmsClient(ITransport transport, DlmsClientOptions options, DlmsClient> logger)
DlmsClient.DlmsClient(ITransport transport, DlmsClientOptions options, ILogger<DlmsClient> logger)
Initializes a new instance of DlmsClient.
Parameters
| Name | Type | Description |
|---|---|---|
transport | ITransport | The transport layer implementation. |
options | SharpMeter.Dlms.DlmsClientOptions | Client configuration options. |
logger | ILogger<SharpMeter.Dlms.DlmsClient> | The logger instance. |
Properties
| Name | Description |
|---|---|
IsAssociated |
Gets whether an application association is currently active. |
IsAssociated
bool DlmsClient.IsAssociated { get; set; }
Gets whether an application association is currently active.
Methods
| Name | Description |
|---|---|
ActionAsync(…) |
Invokes a method on a COSEM object. |
ConnectAsync(CancellationToken cancellationToken) |
Establishes HDLC connection and DLMS application association. |
DisconnectAsync(CancellationToken cancellationToken) |
Gracefully releases the DLMS association and HDLC connection. |
DisposeAsync() |
|
GetAttributeAsync(…) |
Reads an attribute value from a COSEM object identified by OBIS code. |
GetAttributeListAsync(ObisCode ObisCode, byte AttributeIndex)> descriptors, CancellationToken cancellationToken) |
Reads multiple attribute values in a single request. |
SetAttributeAsync(…) |
Writes an attribute value to a COSEM object identified by OBIS code. |
ActionAsync(ushort classId, ObisCode obisCode, byte methodIndex, ReadOnlyMemory? parameters, CancellationToken cancellationToken)
ValueTask<Result<ReadOnlyMemory<byte>>> DlmsClient.ActionAsync(ushort classId, ObisCode obisCode, byte methodIndex, ReadOnlyMemory<byte>? parameters = null, CancellationToken cancellationToken = null)
Invokes a method on a COSEM object.
Parameters
| Name | Type | Description |
|---|---|---|
classId | ushort | The COSEM interface class ID. |
obisCode | SharpMeter.Dlms.Enums.ObisCode | The OBIS code identifying the object. |
methodIndex | byte | The method index to invoke (1-based). |
parameters | ReadOnlyMemory<byte>? | Optional A-XDR encoded method parameters. |
cancellationToken | CancellationToken | Cancellation token. |
Returns: The method return data, or an error.
ConnectAsync(CancellationToken cancellationToken)
ValueTask<Result<bool>> DlmsClient.ConnectAsync(CancellationToken cancellationToken = null)
Establishes HDLC connection and DLMS application association.
Parameters
| Name | Type | Description |
|---|---|---|
cancellationToken | CancellationToken | Cancellation token. |
Returns: A result indicating success or failure.
DisconnectAsync(CancellationToken cancellationToken)
ValueTask DlmsClient.DisconnectAsync(CancellationToken cancellationToken = null)
Gracefully releases the DLMS association and HDLC connection.
Parameters
| Name | Type | Description |
|---|---|---|
cancellationToken | CancellationToken | Cancellation token. |
GetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, CancellationToken cancellationToken)
ValueTask<Result<ReadOnlyMemory<byte>>> DlmsClient.GetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, CancellationToken cancellationToken = null)
Reads an attribute value from a COSEM object identified by OBIS code.
Parameters
| Name | Type | Description |
|---|---|---|
classId | ushort | The COSEM interface class ID. |
obisCode | SharpMeter.Dlms.Enums.ObisCode | The OBIS code identifying the object. |
attributeIndex | byte | The attribute index to read (1-based). |
cancellationToken | CancellationToken | Cancellation token. |
Returns: The raw attribute value bytes, or an error.
GetAttributeListAsync(ObisCode ObisCode, byte AttributeIndex)> descriptors, CancellationToken cancellationToken)
IAsyncEnumerable<Result<ReadOnlyMemory<byte>>> DlmsClient.GetAttributeListAsync(IReadOnlyList<(ushort ClassId, ObisCode ObisCode, byte AttributeIndex)> descriptors, CancellationToken cancellationToken = null)
Reads multiple attribute values in a single request.
Parameters
| Name | Type | Description |
|---|---|---|
descriptors | IReadOnlyList<(ushort ClassId, SharpMeter.Dlms.Enums.ObisCode ObisCode, byte AttributeIndex)> | The list of (classId, obisCode, attributeIndex) tuples. |
cancellationToken | CancellationToken | Cancellation token. |
Returns: An async enumerable of attribute value results.
SetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, ReadOnlyMemory value, CancellationToken cancellationToken)
ValueTask<Result<bool>> DlmsClient.SetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = null)
Writes an attribute value to a COSEM object identified by OBIS code.
Parameters
| Name | Type | Description |
|---|---|---|
classId | ushort | The COSEM interface class ID. |
obisCode | SharpMeter.Dlms.Enums.ObisCode | The OBIS code identifying the object. |
attributeIndex | byte | The attribute index to write (1-based). |
value | ReadOnlyMemory<byte> | The A-XDR encoded value to write. |
cancellationToken | CancellationToken | Cancellation token. |
Returns: A result indicating success or failure.
Type Relationships
classDiagram
style DlmsClient fill:#f9f,stroke:#333,stroke-width:2px
DlmsClient --|> IAsyncDisposable : inherits
View Source
/// <summary>
/// High-level DLMS/COSEM client for communicating with IEC 62056 compliant meters.
/// Supports HDLC transport, association management, and GET/SET/ACTION operations.
/// </summary>
public sealed partial class DlmsClient : IAsyncDisposable
{
#region Constructor
/// <summary>
/// Initializes a new instance of <see cref = "DlmsClient"/>.
/// </summary>
/// <param name = "transport">The transport layer implementation.</param>
/// <param name = "options">Client configuration options.</param>
/// <param name = "logger">The logger instance.</param>
public DlmsClient(ITransport transport, DlmsClientOptions options, ILogger<DlmsClient> logger)
{
_transport = transport;
_options = options;
_logger = logger;
}
#endregion
#region Properties
/// <summary>Gets whether an application association is currently active.</summary>
public bool IsAssociated { get; private set; }
#endregion
#region IAsyncDisposable
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
await DisconnectAsync();
await _transport.DisposeAsync();
}
#endregion
#region SET Operations
/// <summary>
/// Writes an attribute value to a COSEM object identified by OBIS code.
/// </summary>
/// <param name = "classId">The COSEM interface class ID.</param>
/// <param name = "obisCode">The OBIS code identifying the object.</param>
/// <param name = "attributeIndex">The attribute index to write (1-based).</param>
/// <param name = "value">The A-XDR encoded value to write.</param>
/// <param name = "cancellationToken">Cancellation token.</param>
/// <returns>A result indicating success or failure.</returns>
public async ValueTask<Result<bool>> SetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
{
Log.SetAttribute(_logger, obisCode, attributeIndex, classId);
var request = BuildSetRequest(classId, obisCode, attributeIndex, value.Span);
Result<byte[]> result = await SendAndReceiveAsync(request, cancellationToken);
return result.Map(_ => true);
}
#endregion
#region ACTION Operations
/// <summary>
/// Invokes a method on a COSEM object.
/// </summary>
/// <param name = "classId">The COSEM interface class ID.</param>
/// <param name = "obisCode">The OBIS code identifying the object.</param>
/// <param name = "methodIndex">The method index to invoke (1-based).</param>
/// <param name = "parameters">Optional A-XDR encoded method parameters.</param>
/// <param name = "cancellationToken">Cancellation token.</param>
/// <returns>The method return data, or an error.</returns>
public async ValueTask<Result<ReadOnlyMemory<byte>>> ActionAsync(ushort classId, ObisCode obisCode, byte methodIndex, ReadOnlyMemory<byte>? parameters = null, CancellationToken cancellationToken = default)
{
Log.ActionMethod(_logger, obisCode, methodIndex, classId);
ReadOnlySpan<byte> paramSpan = parameters.HasValue ? parameters.Value.Span : ReadOnlySpan<byte>.Empty;
var request = BuildActionRequest(classId, obisCode, methodIndex, paramSpan);
Result<byte[]> result = await SendAndReceiveAsync(request, cancellationToken);
return result.Map(data => (ReadOnlyMemory<byte>)data);
}
#endregion
#region Log Messages
private static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Establishing DLMS connection...")]
public static partial void EstablishingConnection(ILogger logger);
[LoggerMessage(Level = LogLevel.Information, Message = "DLMS association established")]
public static partial void AssociationEstablished(ILogger logger);
[LoggerMessage(Level = LogLevel.Information, Message = "DLMS connection closed")]
public static partial void ConnectionClosed(ILogger logger);
[LoggerMessage(Level = LogLevel.Debug, Message = "GET {ObisCode} attribute {Index} (class {ClassId})")]
public static partial void GetAttribute(ILogger logger, ObisCode obisCode, byte index, ushort classId);
[LoggerMessage(Level = LogLevel.Debug, Message = "SET {ObisCode} attribute {Index} (class {ClassId})")]
public static partial void SetAttribute(ILogger logger, ObisCode obisCode, byte index, ushort classId);
[LoggerMessage(Level = LogLevel.Debug, Message = "ACTION {ObisCode} method {Index} (class {ClassId})")]
public static partial void ActionMethod(ILogger logger, ObisCode obisCode, byte index, ushort classId);
[LoggerMessage(Level = LogLevel.Debug, Message = "SNRM sent, UA received")]
public static partial void SnrmCompleted(ILogger logger);
[LoggerMessage(Level = LogLevel.Debug, Message = "AARQ sent, AARE received")]
public static partial void AarqCompleted(ILogger logger);
}
#endregion
#region Fields
private readonly ITransport _transport;
private readonly ILogger<DlmsClient> _logger;
private readonly DlmsClientOptions _options;
private byte _sendSequence;
private byte _receiveSequence;
#endregion
#region Connection Management
/// <summary>
/// Establishes HDLC connection and DLMS application association.
/// </summary>
/// <param name = "cancellationToken">Cancellation token.</param>
/// <returns>A result indicating success or failure.</returns>
public async ValueTask<Result<bool>> ConnectAsync(CancellationToken cancellationToken = default)
{
Log.EstablishingConnection(_logger);
// Connect transport
Result<bool> transportResult = await _transport.ConnectAsync(cancellationToken);
if (transportResult.IsFailure)
return transportResult.Error;
// Send SNRM (HDLC connection)
Result<bool> snrmResult = await SendHdlcSnrmAsync(cancellationToken);
if (snrmResult.IsFailure)
return snrmResult.Error;
// Send AARQ (Application Association)
Result<bool> aarqResult = await SendAarqAsync(cancellationToken);
if (aarqResult.IsFailure)
return aarqResult.Error;
IsAssociated = true;
Log.AssociationEstablished(_logger);
return true;
}
/// <summary>
/// Gracefully releases the DLMS association and HDLC connection.
/// </summary>
/// <param name = "cancellationToken">Cancellation token.</param>
public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default)
{
if (IsAssociated)
{
// Send RLRQ (Release Request)
await SendRlrqAsync(cancellationToken);
IsAssociated = false;
}
// Send DISC (HDLC disconnect)
await SendHdlcDiscAsync(cancellationToken);
await _transport.DisconnectAsync(cancellationToken);
Log.ConnectionClosed(_logger);
}
#endregion
#region GET Operations
/// <summary>
/// Reads an attribute value from a COSEM object identified by OBIS code.
/// </summary>
/// <param name = "classId">The COSEM interface class ID.</param>
/// <param name = "obisCode">The OBIS code identifying the object.</param>
/// <param name = "attributeIndex">The attribute index to read (1-based).</param>
/// <param name = "cancellationToken">Cancellation token.</param>
/// <returns>The raw attribute value bytes, or an error.</returns>
public async ValueTask<Result<ReadOnlyMemory<byte>>> GetAttributeAsync(ushort classId, ObisCode obisCode, byte attributeIndex, CancellationToken cancellationToken = default)
{
Log.GetAttribute(_logger, obisCode, attributeIndex, classId);
var request = BuildGetRequest(classId, obisCode, attributeIndex);
Result<byte[]> result = await SendAndReceiveAsync(request, cancellationToken);
return result.Map(data => (ReadOnlyMemory<byte>)data);
}
/// <summary>
/// Reads multiple attribute values in a single request.
/// </summary>
/// <param name = "descriptors">The list of (classId, obisCode, attributeIndex) tuples.</param>
/// <param name = "cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of attribute value results.</returns>
public async IAsyncEnumerable<Result<ReadOnlyMemory<byte>>> GetAttributeListAsync(IReadOnlyList<(ushort ClassId, ObisCode ObisCode, byte AttributeIndex)> descriptors, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(descriptors);
foreach ((var classId, ObisCode obis, var attr)in descriptors)
yield return await GetAttributeAsync(classId, obis, attr, cancellationToken);
}
#endregion
#region Private HDLC Methods
private async ValueTask<Result<bool>> SendHdlcSnrmAsync(CancellationToken cancellationToken)
{
var snrm = new HdlcFrame
{
Destination = _options.ServerAddress,
Source = _options.ClientAddress,
Control = HdlcControl.Snrm,
Information = ReadOnlyMemory<byte>.Empty
};
var frameBytes = snrm.Encode();
Result<bool> sendResult = await _transport.SendAsync(frameBytes, cancellationToken);
if (sendResult.IsFailure)
return sendResult.Error;
var buffer = new byte[256];
Result<int> receiveResult = await _transport.ReceiveAsync(buffer, cancellationToken);
if (receiveResult.IsFailure)
return receiveResult.Error;
Log.SnrmCompleted(_logger);
return true;
}
private async ValueTask<Result<bool>> SendAarqAsync(CancellationToken cancellationToken)
{
var aarq = BuildAarqApdu();
Result<byte[]> result = await SendInfoFrameAsync(aarq, cancellationToken);
if (result.IsFailure)
return result.Error;
// Parse AARE response — check association-result
var aare = result.Value;
if (aare.Length < 2 || aare[0] != (byte)DlmsApduTag.Aare)
return new PsemError(PsemErrorCode.ProtocolError, "Invalid AARE response");
// Search for association-result tag [A2] in the AARE body
Span<byte> body = aare.AsSpan(2); // skip tag + length
for (var i = 0; i < body.Length - 2; i++)
if (body[i] == 0xA2 && i + 2 < body.Length)
{
// [A2] len [02] len result-value
var resultOffset = i + 2;
if (resultOffset + 2 < body.Length && body[resultOffset] == 0x02)
{
var associationResult = body[resultOffset + 2];
if (associationResult != 0) // 0 = accepted
return new PsemError(PsemErrorCode.ProtocolError, $"DLMS association rejected: result={associationResult}");
}
break;
}
Log.AarqCompleted(_logger);
return true;
}
private async ValueTask SendRlrqAsync(CancellationToken cancellationToken)
{
byte[] rlrq = [(byte)DlmsApduTag.Rlrq, 0x00];
await SendInfoFrameAsync(rlrq, cancellationToken);
}
private async ValueTask SendHdlcDiscAsync(CancellationToken cancellationToken)
{
var disc = new HdlcFrame
{
Destination = _options.ServerAddress,
Source = _options.ClientAddress,
Control = HdlcControl.Disc,
Information = ReadOnlyMemory<byte>.Empty
};
await _transport.SendAsync(disc.Encode(), cancellationToken);
}
#endregion
#region Private Framing Methods
private async ValueTask<Result<byte[]>> SendInfoFrameAsync(byte[] apdu, CancellationToken cancellationToken)
{
var frame = new HdlcFrame
{
Destination = _options.ServerAddress,
Source = _options.ClientAddress,
Control = new HdlcControl(HdlcFrameType.Information, true, _sendSequence, _receiveSequence),
Information = apdu
};
_sendSequence = (byte)((_sendSequence + 1) & 0x07);
var frameBytes = frame.Encode();
Result<bool> sendResult = await _transport.SendAsync(frameBytes, cancellationToken);
if (sendResult.IsFailure)
return sendResult.Error;
var buffer = new byte[4096];
Result<int> receiveResult = await _transport.ReceiveAsync(buffer, cancellationToken);
if (receiveResult.IsFailure)
return receiveResult.Error;
var bytesRead = receiveResult.Value;
if (bytesRead == 0)
return PsemError.Transport("No response received");
Result<HdlcFrame> responseFrame = HdlcFrame.Decode(buffer.AsSpan(0, bytesRead));
if (responseFrame.IsFailure)
return responseFrame.Error;
_receiveSequence = (byte)((_receiveSequence + 1) & 0x07);
return responseFrame.Value.Information.ToArray();
}
private async ValueTask<Result<byte[]>> SendAndReceiveAsync(byte[] apdu, CancellationToken cancellationToken) => await SendInfoFrameAsync(apdu, cancellationToken);
#endregion
#region Private APDU Builders
private byte[] BuildAarqApdu()
{
var apdu = new List<byte>
{
(byte)DlmsApduTag.Aarq,
0x00 // length placeholder
};
// Application context name (LN referencing with no ciphering: {2 16 756 5 8 1 1})
byte[] appContext = [0xA1, 0x09, 0x06, 0x07, 0x60, 0x85, 0x74, 0x05, 0x08, 0x01, 0x01];
apdu.AddRange(appContext);
// Authentication mechanism (if configured)
if (_options.Authentication != AuthenticationMechanism.None)
{
// Sender ACSE requirements — authentication functional unit
byte[] acseRequirements = [0x8A, 0x02, 0x07, 0x80];
apdu.AddRange(acseRequirements);
// Mechanism name OID ({2 16 756 5 8 2 <auth-level>})
byte[] mechName = [0x8B, 0x07, 0x60, 0x85, 0x74, 0x05, 0x08, 0x02, (byte)_options.Authentication];
apdu.AddRange(mechName);
// Calling authentication value (password)
if (_options.Password.Length > 0)
{
// [AC] context tag for calling-authentication-value
apdu.Add(0xAC);
apdu.Add((byte)(_options.Password.Length + 2));
// [80] GraphicString choice
apdu.Add(0x80);
apdu.Add((byte)_options.Password.Length);
apdu.AddRange(_options.Password);
}
}
// User-information with xDLMS InitiateRequest
var initiateRequest = BuildInitiateRequest();
// [BE] user-information tag
apdu.Add(0xBE);
apdu.Add((byte)(initiateRequest.Length + 2));
// [04] OCTET STRING wrapper
apdu.Add(0x04);
apdu.Add((byte)initiateRequest.Length);
apdu.AddRange(initiateRequest);
// Fix length
var result = apdu.ToArray();
result[1] = (byte)(result.Length - 2);
return result;
}
private byte[] BuildInitiateRequest()
{
// xDLMS InitiateRequest APDU
return[0x01, // initiate-request tag
0x00, // dedicated-key: absent
0x00, // response-allowed: true (default)
0x00, // proposed-quality-of-service: unused
0x06, // proposed-dlms-version-number: 6
// proposed-conformance (tag + 4 bytes: [5F 1F] + length + 3 conformance bytes)
0x5F, 0x1F, 0x04, 0x00, // unused conformance bits
0x00, 0x1E, 0x1D, // GET, SET, ACTION, selective-access, block-transfer
// client-max-receive-pdu-size (2 bytes big-endian)
(byte)(_options.MaxPduSize >> 8), (byte)(_options.MaxPduSize & 0xFF)];
}
private static byte[] BuildGetRequest(ushort classId, ObisCode obisCode, byte attributeIndex)
{
return[(byte)DlmsApduTag.GetRequest, (byte)GetRequestType.Normal, 0x01, // invoke-id-and-priority
// Cosem-Attribute-Descriptor (9 bytes)
(byte)(classId >> 8), (byte)(classId & 0xFF), obisCode.A, obisCode.B, obisCode.C, obisCode.D, obisCode.E, obisCode.F, attributeIndex, 0x00 // access-selection: none
];
}
private static byte[] BuildSetRequest(ushort classId, ObisCode obisCode, byte attributeIndex, ReadOnlySpan<byte> value)
{
// SET-Request-Normal: tag + type + invoke-id + cosem-attribute-descriptor(9) + access-selection + value
var request = new byte[4 + 9 + value.Length];
var offset = 0;
request[offset++] = (byte)DlmsApduTag.SetRequest;
request[offset++] = (byte)SetRequestType.Normal;
request[offset++] = 0x01; // invoke-id-and-priority
// Cosem-Attribute-Descriptor (9 bytes)
request[offset++] = (byte)(classId >> 8);
request[offset++] = (byte)(classId & 0xFF);
request[offset++] = obisCode.A;
request[offset++] = obisCode.B;
request[offset++] = obisCode.C;
request[offset++] = obisCode.D;
request[offset++] = obisCode.E;
request[offset++] = obisCode.F;
request[offset++] = attributeIndex;
// Access-selection: none
request[offset++] = 0x00;
// Value
value.CopyTo(request.AsSpan(offset));
return request;
}
private static byte[] BuildActionRequest(ushort classId, ObisCode obisCode, byte methodIndex, ReadOnlySpan<byte> parameters)
{
var hasParams = parameters.Length > 0;
// ACTION-Request-Normal: tag + type + invoke-id + cosem-method-descriptor(9) + optional-data
var request = new byte[3 + 9 + 1 + (hasParams ? parameters.Length : 0)];
var offset = 0;
request[offset++] = (byte)DlmsApduTag.ActionRequest;
request[offset++] = (byte)ActionRequestType.Normal;
request[offset++] = 0x01; // invoke-id-and-priority
// Cosem-Method-Descriptor (9 bytes): class-id(2) + instance-id(6) + method-id(1)
request[offset++] = (byte)(classId >> 8);
request[offset++] = (byte)(classId & 0xFF);
request[offset++] = obisCode.A;
request[offset++] = obisCode.B;
request[offset++] = obisCode.C;
request[offset++] = obisCode.D;
request[offset++] = obisCode.E;
request[offset++] = obisCode.F;
request[offset++] = methodIndex;
if (hasParams)
{
request[offset++] = 0x01; // optional-data = present
parameters.CopyTo(request.AsSpan(offset));
}
else
{
request[offset] = 0x00; // optional-data = absent
}
return request;
}
#endregion
}