Binary Serialization

SharpMeter is a wire protocol library — all data is manually packed and unpacked using Span<T>, ReadOnlyMemory<byte>, and System.Buffers. No third-party serialization is used.

Reading Table Fields

Table data from ReadTableAsync is raw bytes. Parse fields by offset and size per the ANSI C12.19 table definitions:

// Read ST1: General Manufacturer ID Table (32 bytes)
var result = await client.ReadTableAsync(1);
if (result.IsFailure) return;

var data = result.Value.Data.Span;

// ST1 field layout:
// Offset 0-3:   MANUFACTURER (4 bytes, ASCII)
// Offset 4-11:  ED_MODEL (8 bytes, ASCII)
// Offset 12:    HW_VERSION_NUMBER (1 byte)
// Offset 13:    HW_REVISION_NUMBER (1 byte)
// Offset 14:    FW_VERSION_NUMBER (1 byte)
// Offset 15:    FW_REVISION_NUMBER (1 byte)
// Offset 16-31: MFG_SERIAL_NUMBER (16 bytes, ASCII)

var manufacturer = System.Text.Encoding.ASCII.GetString(data[..4]).TrimEnd('\0');
var model = System.Text.Encoding.ASCII.GetString(data[4..12]).TrimEnd('\0');
var hwVersion = data[12];
var hwRevision = data[13];
var fwVersion = data[14];
var fwRevision = data[15];
var serial = System.Text.Encoding.ASCII.GetString(data[16..32]).TrimEnd('\0');

Writing Table Fields

Build table data with manual byte packing:

// Write to ST52: Clock Table (7 bytes)
var now = DateTime.UtcNow;
var clockData = new byte[7];
clockData[0] = (byte)(now.Year - 2000);
clockData[1] = (byte)now.Month;
clockData[2] = (byte)now.Day;
clockData[3] = (byte)now.Hour;
clockData[4] = (byte)now.Minute;
clockData[5] = (byte)now.Second;
clockData[6] = 0; // Time/date qualifier

await client.WriteTableAsync(52, clockData);

Big-Endian Integer Encoding

PSEM uses big-endian byte order for multi-byte integers:

// Encode a 16-bit table ID
ushort tableId = 2048;
byte[] encoded = [(byte)(tableId >> 8), (byte)(tableId & 0xFF)];
// Result: [0x08, 0x00]

// Decode a 16-bit value from response
ushort decoded = (ushort)((data[0] << 8) | data[1]);

// 24-bit offset (3 bytes)
int offset = 0x01ABCD;
byte[] offsetBytes = [
    (byte)((offset >> 16) & 0xFF),
    (byte)((offset >> 8) & 0xFF),
    (byte)(offset & 0xFF)
];

Bit Field Parsing

Many PSEM tables use packed bit fields:

// Parse ST0 FORMAT_CONTROL_1 (1 byte):
// Bits 0:    DATA_ORDER (1 bit)
// Bits 1-3:  CHAR_FORMAT (3 bits)
// Bits 4-6:  MODEL_SELECT (3 bits)
// Bit 7:     FILL (1 bit)

byte formatControl1 = data[0];
var dataOrder = (formatControl1 >> 0) & 0x01;      // 0=Little Endian, 1=Big Endian
var charFormat = (formatControl1 >> 1) & 0x07;      // 0-7
var modelSelect = (formatControl1 >> 4) & 0x07;     // 0-7

// Parse StandardStatusFlags from ST3 (2 bytes, little-endian)
var statusBytes = data[1..3];
var flags = (StandardStatusFlags)((statusBytes[1] << 8) | statusBytes[0]);
if (flags.HasFlag(StandardStatusFlags.PowerFailure))
    Console.WriteLine("Power failure detected");

Procedure Parameter Encoding

Build procedure parameters as raw byte arrays:

// SP 10: Set Date/Time
// Parameters: Year(1) Month(1) Day(1) Hour(1) Minute(1) Second(1) DST(1) GMT(1)
var now = DateTime.UtcNow;
byte[] setDateTimeParams = [
    (byte)(now.Year - 2000),
    (byte)now.Month,
    (byte)now.Day,
    (byte)now.Hour,
    (byte)now.Minute,
    (byte)now.Second,
    0x00, // DST flag
    0x01  // GMT flag
];

await client.ExecuteStandardProcedureAsync(10, setDateTimeParams);

CRC-16 Calculation

Use Crc16 directly for custom framing needs:

using SharpMeter.Core.Crc;

byte[] data = [0x01, 0x02, 0x03, 0x04];
ushort crc = Crc16.Compute(data);

// Verify a frame's CRC
bool valid = Crc16.Verify(frameBytes);

DLMS A-XDR Encoding

For DLMS/COSEM, use AxdrEncoder for type-safe encoding:

using SharpMeter.Dlms.Codec;

// Encode values
byte[] uint32Value = AxdrEncoder.EncodeDoubleLongUnsigned(42000);
byte[] stringValue = AxdrEncoder.EncodeVisibleString("Hello");
byte[] octetValue = AxdrEncoder.EncodeOctetString(new byte[] { 0x01, 0x02 });
byte[] boolValue = AxdrEncoder.EncodeBoolean(true);
byte[] enumValue = AxdrEncoder.EncodeEnum(0x01);

// Decode values
uint decoded = AxdrEncoder.DecodeDoubleLongUnsigned(responseData);
string text = AxdrEncoder.DecodeVisibleString(responseData, out int consumed);
DateTime dt = AxdrEncoder.DecodeDateTime(dateTimeBytes);

OBIS Code Handling

using SharpMeter.Dlms.Enums;

// Well-known codes
var energy = ObisCode.ActiveEnergyImportTotal; // 1-0:1.8.0*255

// Parse from string
var custom = ObisCode.Parse("1-0:1.8.0*255");

// Encode to bytes for wire transmission
byte[] obisBytes = energy.ToBytes(); // [1, 0, 1, 8, 0, 255]

// Decode from bytes
var fromWire = ObisCode.FromBytes(receivedBytes);

Zero-Allocation Patterns

SharpMeter minimizes heap allocations on hot paths:

// Span-based CRC — no allocation
ReadOnlySpan<byte> frameData = stackalloc byte[] { 0x00, 0x40, 0x00, 0x00, 0x01, 0x20 };
ushort crc = Crc16.Compute(frameData);

// ArrayPool for temporary buffers
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
    var receiveResult = await transport.ReceiveAsync(buffer);
    // Process data...
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

// ReadOnlyMemory<byte> avoids copies
var tableData = result.Value.Data; // No copy — shares underlying array
var slice = tableData[4..12];      // No copy — just a view
Last updated: 2026-04-08
Was this page helpful?