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
| Name | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
data | ReadOnlyMemory<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<ManufacturerIdTable>(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
}