Class Static
public static class TableMapper

Namespace: SharpMeter.Core.Tables

Deserializes raw PSEM table bytes into strongly-typed C# objects using attribute-based mapping.

Remarks

Decorate a class or record with PsemTableAttribute and its properties with TableFieldAttribute to enable automatic deserialization:


[PsemTable(1, "Manufacturer ID", ExpectedSize = 32)]
public sealed record ManufacturerIdTable
{
    [TableField(0, 4, FieldType.Ascii)]
    public string Manufacturer { get; init; }

    [TableField(12, 1, FieldType.UInt8)]
    public byte HwVersion { get; init; }
}

var table = TableMapper.Deserialize(rawBytes);

Methods

NameDescription
BuildDefinition() static Builds a TableDefinition from the attribute metadata on a type. Useful for registering attribute-defined tables in a TableRegistry.
Deserialize(ReadOnlyMemory<byte> data) static Deserializes raw bytes into a strongly-typed table object using attribute metadata.

BuildDefinition()

Result<TableDefinition> TableMapper.BuildDefinition<T>()

Builds a TableDefinition from the attribute metadata on a type. Useful for registering attribute-defined tables in a TableRegistry.

Returns: The table definition, or an error if the type lacks the required attribute.

Deserialize(ReadOnlyMemory data)

Result<T> TableMapper.Deserialize<T>(ReadOnlyMemory<byte> data)

Deserializes raw bytes into a strongly-typed table object using attribute metadata.

Parameters

NameTypeDescription
dataReadOnlyMemory<byte>The raw table bytes.

Returns: A populated instance of T, or an error.

View Source
/// <summary>
///     Deserializes raw PSEM table bytes into strongly-typed C# objects using attribute-based mapping.
/// </summary>
/// <remarks>
///     <para>
///         Decorate a class or record with <see cref = "PsemTableAttribute"/> and its properties
///         with <see cref = "TableFieldAttribute"/> to enable automatic deserialization:
///     </para>
///     <code>
/// [PsemTable(1, "Manufacturer ID", ExpectedSize = 32)]
/// public sealed record ManufacturerIdTable
/// {
///     [TableField(0, 4, FieldType.Ascii)]
///     public string Manufacturer { get; init; }
/// 
///     [TableField(12, 1, FieldType.UInt8)]
///     public byte HwVersion { get; init; }
/// }
/// 
/// var table = TableMapper.Deserialize&lt;ManufacturerIdTable&gt;(rawBytes);
/// </code>
/// </remarks>
public static class TableMapper
{
#region Public Methods
    /// <summary>
    ///     Deserializes raw bytes into a strongly-typed table object using attribute metadata.
    /// </summary>
    /// <typeparam name = "T">The table type decorated with <see cref = "PsemTableAttribute"/>.</typeparam>
    /// <param name = "data">The raw table bytes.</param>
    /// <returns>A populated instance of <typeparamref name = "T"/>, or an error.</returns>
    public static Result<T> Deserialize<T>(ReadOnlyMemory<byte> data)
        where T : new()
    {
        PsemTableAttribute? tableAttr = typeof(T).GetCustomAttribute<PsemTableAttribute>();
        if (tableAttr is null)
            return PsemError.Framing($"Type {typeof(T).Name} is missing [PsemTable] attribute");
        if (tableAttr.ExpectedSize > 0 && data.Length < tableAttr.ExpectedSize)
            return PsemError.Framing($"Table {tableAttr.Name} expected {tableAttr.ExpectedSize} bytes, got {data.Length}");
        var instance = new T();
        ReadOnlySpan<byte> span = data.Span;
        foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            TableFieldAttribute? fieldAttr = prop.GetCustomAttribute<TableFieldAttribute>();
            if (fieldAttr is null)
                continue;
            if (fieldAttr.Offset + fieldAttr.Size > data.Length)
                continue; // Partial table — skip missing fields
            ReadOnlySpan<byte> fieldBytes = span.Slice(fieldAttr.Offset, fieldAttr.Size);
            var value = ConvertField(fieldAttr.Type, fieldBytes, prop.PropertyType);
            if (value is not null && prop.CanWrite)
                prop.SetValue(instance, value);
        }

        return instance;
    }

    /// <summary>
    ///     Builds a <see cref = "TableDefinition"/> from the attribute metadata on a type.
    ///     Useful for registering attribute-defined tables in a <see cref = "TableRegistry"/>.
    /// </summary>
    /// <typeparam name = "T">The table type decorated with <see cref = "PsemTableAttribute"/>.</typeparam>
    /// <returns>The table definition, or an error if the type lacks the required attribute.</returns>
    public static Result<TableDefinition> BuildDefinition<T>()
    {
        PsemTableAttribute? tableAttr = typeof(T).GetCustomAttribute<PsemTableAttribute>();
        if (tableAttr is null)
            return PsemError.Framing($"Type {typeof(T).Name} is missing [PsemTable] attribute");
        ImmutableArray<TableFieldDefinition>.Builder fields = ImmutableArray.CreateBuilder<TableFieldDefinition>();
        foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            TableFieldAttribute? fieldAttr = prop.GetCustomAttribute<TableFieldAttribute>();
            if (fieldAttr is null)
                continue;
            fields.Add(new TableFieldDefinition { Name = prop.Name, Offset = fieldAttr.Offset, Size = fieldAttr.Size, Type = fieldAttr.Type, Description = fieldAttr.Description, IsReadOnly = fieldAttr.IsReadOnly });
        }

        // Sort by offset
        fields.Sort((a, b) => a.Offset.CompareTo(b.Offset));
        return new TableDefinition
        {
            TableId = tableAttr.TableId,
            Name = tableAttr.Name,
            Description = tableAttr.Description,
            ExpectedSize = tableAttr.ExpectedSize,
            Fields = fields.ToImmutable()
        };
    }

#endregion
#region Field Conversion
    private static object? ConvertField(FieldType type, ReadOnlySpan<byte> bytes, Type targetType)
    {
        return type switch
        {
            FieldType.Ascii when targetType == typeof(string) => Encoding.ASCII.GetString(bytes).TrimEnd('\0', ' '),
            FieldType.String when targetType == typeof(string) => Encoding.UTF8.GetString(bytes).TrimEnd('\0'),
            FieldType.Hex when targetType == typeof(string) => Convert.ToHexString(bytes),
            FieldType.Hex when targetType == typeof(byte[]) => bytes.ToArray(),
            FieldType.UInt8 when bytes.Length >= 1 => ConvertNumeric(bytes[0], targetType),
            FieldType.UInt16 when bytes.Length >= 2 => ConvertNumeric((ushort)((bytes[0] << 8) | bytes[1]), targetType),
            FieldType.UInt32 when bytes.Length >= 4 => ConvertNumeric((uint)((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]), targetType),
            FieldType.Int8 when bytes.Length >= 1 => ConvertNumeric((sbyte)bytes[0], targetType),
            FieldType.Int16 when bytes.Length >= 2 => ConvertNumeric((short)((bytes[0] << 8) | bytes[1]), targetType),
            FieldType.Int32 when bytes.Length >= 4 => ConvertNumeric((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3], targetType),
            FieldType.Boolean when bytes.Length >= 1 => bytes[0] != 0,
            FieldType.DateTime when bytes.Length >= 6 => new DateTime(2000 + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], DateTimeKind.Utc),
            _ => bytes.ToArray()};
    }

    [SuppressMessage("Performance", "CA1859", Justification = "Must return boxed typed values for PropertyInfo.SetValue")]
    private static object ConvertNumeric(long value, Type targetType)
    {
        if (targetType == typeof(byte))
            return (byte)value;
        if (targetType == typeof(sbyte))
            return (sbyte)value;
        if (targetType == typeof(short))
            return (short)value;
        if (targetType == typeof(ushort))
            return (ushort)value;
        if (targetType == typeof(int))
            return (int)value;
        if (targetType == typeof(uint))
            return (uint)value;
        if (targetType == typeof(long))
            return value;
        if (targetType == typeof(ulong))
            return (ulong)value;
        if (targetType == typeof(float))
            return (float)value;
        if (targetType == typeof(double))
            return (double)value;
        return value;
    }
#endregion
}
Was this page helpful?