Record
public record PsemFrame : System.ValueType, System.IEquatable<SharpMeter.Core.Protocol.PsemFrame>
Namespace: SharpMeter.Core.Protocol
Represents an ANSI C12.18 L2 PSEM frame.
Remarks
Frame structure: [STP 0xEE] [Reserved 0x00] [Control] [Sequence] [LenH] [LenL] [Data...] [CRCH] [CRCL]
Inheritance
Inherits from: System.ValueType
Implements: System.IEquatable<SharpMeter.Core.Protocol.PsemFrame>
Properties
| Name | Description |
|---|---|
Control |
Gets the control byte information. |
Crc |
Gets the CRC-16 stored in the frame. |
Data |
Gets the L7 payload data. |
Sequence |
Gets the sequence number. |
TotalLength |
Gets the total frame length including overhead. |
Control
ControlByte PsemFrame.Control { get; init; }
Gets the control byte information.
Crc
ushort PsemFrame.Crc { get; init; }
Gets the CRC-16 stored in the frame.
Data
ReadOnlyMemory<byte> PsemFrame.Data { get; init; }
Gets the L7 payload data.
Sequence
byte PsemFrame.Sequence { get; init; }
Gets the sequence number.
TotalLength
int PsemFrame.TotalLength { get; }
Gets the total frame length including overhead.
Methods
| Name | Description |
|---|---|
Decode(ReadOnlySpan<byte> source) static |
Attempts to decode a PSEM frame from raw bytes. |
Encode(Span<byte> destination) |
Encodes this frame into a byte buffer for wire transmission. |
Decode(ReadOnlySpan source)
Result<PsemFrame> PsemFrame.Decode(ReadOnlySpan<byte> source)
Attempts to decode a PSEM frame from raw bytes.
Parameters
| Name | Type | Description |
|---|---|---|
source | ReadOnlySpan<byte> | The raw frame bytes. |
Returns: A result indicating success or the decoding error.
Encode(Span destination)
int PsemFrame.Encode(Span<byte> destination)
Encodes this frame into a byte buffer for wire transmission.
Parameters
| Name | Type | Description |
|---|---|---|
destination | Span<byte> | The destination span. Must be at least PsemFrame.TotalLength bytes. |
Returns: The number of bytes written.
Fields
| Name | Description |
|---|---|
Ack static |
ACK response byte. |
Can static |
CAN (cancel) response byte. |
Nak static |
NAK response byte. |
OverheadSize static |
L2 overhead: STP(1) + Reserved(1) + Control(1) + Seq(1) + Length(2) + CRC(2) = 8 bytes. |
Reserved static |
Reserved byte value (always 0x00). |
Stp static |
Start-of-packet marker byte. |
Ack
byte Ack
ACK response byte.
Can
byte Can
CAN (cancel) response byte.
Nak
byte Nak
NAK response byte.
OverheadSize
int OverheadSize
L2 overhead: STP(1) + Reserved(1) + Control(1) + Seq(1) + Length(2) + CRC(2) = 8 bytes.
Reserved
byte Reserved
Reserved byte value (always 0x00).
Stp
byte Stp
Start-of-packet marker byte.
Type Relationships
classDiagram
style PsemFrame fill:#f9f,stroke:#333,stroke-width:2px
PsemFrame --|> ValueType : inherits
PsemFrame ..|> PsemFrame~ : implements
View Source
/// <summary>
/// Represents an ANSI C12.18 L2 PSEM frame.
/// </summary>
/// <remarks>
/// Frame structure: [STP 0xEE] [Reserved 0x00] [Control] [Sequence] [LenH] [LenL] [Data...] [CRCH] [CRCL]
/// </remarks>
public readonly record struct PsemFrame
{
#region Constants
/// <summary>Start-of-packet marker byte.</summary>
public const byte Stp = 0xEE;
/// <summary>Reserved byte value (always 0x00).</summary>
public const byte Reserved = 0x00;
/// <summary>L2 overhead: STP(1) + Reserved(1) + Control(1) + Seq(1) + Length(2) + CRC(2) = 8 bytes.</summary>
public const int OverheadSize = 8;
/// <summary>ACK response byte.</summary>
public const byte Ack = 0x06;
/// <summary>NAK response byte.</summary>
public const byte Nak = 0x15;
/// <summary>CAN (cancel) response byte.</summary>
public const byte Can = 0x18;
#endregion
#region Properties
/// <summary>Gets the control byte information.</summary>
public ControlByte Control { get; init; }
/// <summary>Gets the sequence number.</summary>
public byte Sequence { get; init; }
/// <summary>Gets the L7 payload data.</summary>
public ReadOnlyMemory<byte> Data { get; init; }
/// <summary>Gets the CRC-16 stored in the frame.</summary>
public ushort Crc { get; init; }
/// <summary>Gets the total frame length including overhead.</summary>
public int TotalLength => OverheadSize + Data.Length;
#endregion
#region Methods
/// <summary>
/// Encodes this frame into a byte buffer for wire transmission.
/// </summary>
/// <param name = "destination">The destination span. Must be at least <see cref = "TotalLength"/> bytes.</param>
/// <returns>The number of bytes written.</returns>
public int Encode(Span<byte> destination)
{
ReadOnlySpan<byte> dataSpan = Data.Span;
var totalLen = OverheadSize + dataSpan.Length;
if (destination.Length < totalLen)
throw new ArgumentException($"Destination too small: need {totalLen}, got {destination.Length}");
destination[0] = Stp;
destination[1] = Reserved;
destination[2] = Control.Encode();
destination[3] = Sequence;
destination[4] = (byte)(dataSpan.Length >> 8);
destination[5] = (byte)(dataSpan.Length & 0xFF);
dataSpan.CopyTo(destination[6..]);
var crc = Crc16.Compute(destination[1..(6 + dataSpan.Length)]);
var swapped = (ushort)((crc >> 8) | (crc << 8));
destination[6 + dataSpan.Length] = (byte)(swapped >> 8);
destination[7 + dataSpan.Length] = (byte)(swapped & 0xFF);
return totalLen;
}
/// <summary>
/// Attempts to decode a PSEM frame from raw bytes.
/// </summary>
/// <param name = "source">The raw frame bytes.</param>
/// <returns>A result indicating success or the decoding error.</returns>
public static Result<PsemFrame> Decode(ReadOnlySpan<byte> source)
{
if (source.Length < OverheadSize)
return PsemError.Framing($"Frame too short: {source.Length} bytes, minimum {OverheadSize}");
if (source[0] != Stp)
return PsemError.Framing($"Invalid STP byte: 0x{source[0]:X2}, expected 0x{Stp:X2}");
var control = ControlByte.Decode(source[2]);
var sequence = source[3];
var dataLength = (source[4] << 8) | source[5];
if (source.Length < OverheadSize + dataLength)
return PsemError.Framing($"Frame truncated: expected {OverheadSize + dataLength} bytes, got {source.Length}");
var data = source.Slice(6, dataLength).ToArray();
var storedCrc = (ushort)((source[6 + dataLength] << 8) | source[7 + dataLength]);
var computed = Crc16.Compute(source[1..(6 + dataLength)]);
var swapped = (ushort)((computed >> 8) | (computed << 8));
if (swapped != storedCrc)
return PsemError.CrcMismatch(swapped, storedCrc);
return new PsemFrame
{
Control = control,
Sequence = sequence,
Data = data,
Crc = storedCrc
};
}
#endregion
}