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

NameDescription
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

NameDescription
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

NameTypeDescription
sourceReadOnlySpan<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

NameTypeDescription
destinationSpan<byte>The destination span. Must be at least PsemFrame.TotalLength bytes.

Returns: The number of bytes written.

Fields

NameDescription
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
}
Was this page helpful?