feat(s7): byte-buffer codec dispatch + Int64/UInt64/LReal scalar read+write
This commit is contained in:
@@ -499,6 +499,14 @@ public sealed class S7Driver
|
||||
if (tag.ArrayCount is >= 1)
|
||||
return await ReadArrayAsync(plc, tag, addr, ct).ConfigureAwait(false);
|
||||
|
||||
// Wide/structured scalar path (Int64/UInt64/Float64/String/DateTime): S7.Net's string
|
||||
// ReadAsync only decodes 1/2/4-byte size suffixes, so these can't go through the narrow
|
||||
// path below. Read a contiguous byte block at the start byte and decode it big-endian —
|
||||
// mirrors the array path's buffer read. Timer/Counter are excluded here (IsBufferType
|
||||
// gates on Area) and keep using the narrow path for now (broadened in a follow-up).
|
||||
if (IsBufferType(tag, addr))
|
||||
return await ReadScalarBlockAsync(plc, tag, addr, ct).ConfigureAwait(false);
|
||||
|
||||
// S7.Net's string-based ReadAsync returns object where the boxed .NET type depends on
|
||||
// the size suffix: DBX=bool, DBB=byte, DBW=ushort, DBD=uint. Our S7DataType enum
|
||||
// specifies the SEMANTIC type (Int16 vs UInt16 vs Float32 etc.); the reinterpret below
|
||||
@@ -536,6 +544,134 @@ public sealed class S7Driver
|
||||
return DecodeArrayBlock(tag, addr, block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when this tag must be read/written through the byte-buffer codec rather than
|
||||
/// S7.Net's string path. The wide/structured types (Int64/UInt64/Float64/String/DateTime)
|
||||
/// are byte-anchored (<c>DBB</c>/<c>MB</c>/<c>IB</c>/<c>QB</c>) and decoded from a
|
||||
/// contiguous block — S7.Net's string ReadAsync only understands 1/2/4-byte size
|
||||
/// suffixes, so they can't use the narrow path. Timer/Counter areas are deliberately
|
||||
/// excluded (they still route through the narrow path for now); a follow-up adds them to
|
||||
/// this seam. The init guard already enforces byte-addressing + the Timer→Float64 /
|
||||
/// Counter→Int32 type constraints, so by the time a tag reaches here it is well-formed.
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag definition carrying the <see cref="S7DataType"/>.</param>
|
||||
/// <param name="addr">Parsed address — its <see cref="S7Area"/> excludes Timer/Counter from the seam.</param>
|
||||
/// <returns><c>true</c> if the tag routes through the byte-buffer codec.</returns>
|
||||
private static bool IsBufferType(S7TagDefinition tag, S7ParsedAddress addr) =>
|
||||
addr.Area is not S7Area.Timer and not S7Area.Counter
|
||||
&& tag.DataType is S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64
|
||||
or S7DataType.String or S7DataType.DateTime;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a wide/structured scalar as ONE contiguous byte block via S7.Net's
|
||||
/// buffer-based <c>Plc.ReadBytesAsync(DataType, db, startByteAdr, count, ct)</c> (a single
|
||||
/// PLC round-trip), then hands the raw block to the pure <see cref="DecodeScalarBlock"/>
|
||||
/// decode. Mirrors <see cref="ReadArrayAsync"/>'s shape.
|
||||
/// </summary>
|
||||
private async Task<object> ReadScalarBlockAsync(Plc plc, S7TagDefinition tag, S7ParsedAddress addr, CancellationToken ct)
|
||||
{
|
||||
var width = ScalarByteWidth(tag);
|
||||
var block = await plc.ReadBytesAsync(ToS7NetArea(addr.Area), addr.DbNumber, addr.ByteOffset, width, ct)
|
||||
.ConfigureAwait(false)
|
||||
?? throw new System.IO.InvalidDataException($"S7.Net returned null block for '{tag.Address}'");
|
||||
|
||||
return DecodeScalarBlock(tag, addr, block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Byte width of one wide/structured scalar. Int64/UInt64/Float64/DateTime are 8 bytes;
|
||||
/// an S7 STRING occupies <c>StringLength + 2</c> bytes (the two-byte header carries the
|
||||
/// declared max length and the current actual length). Timer/Counter widths land in a
|
||||
/// follow-up. Throws for any non-buffer type — defensive; <see cref="IsBufferType"/> gates
|
||||
/// this so a non-buffer type never reaches here.
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag definition carrying the <see cref="S7DataType"/> and (for strings) <c>StringLength</c>.</param>
|
||||
/// <returns>The byte width to read/write for this scalar.</returns>
|
||||
internal static int ScalarByteWidth(S7TagDefinition tag) => tag.DataType switch
|
||||
{
|
||||
S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64 => 8,
|
||||
S7DataType.DateTime => 8,
|
||||
S7DataType.String => tag.StringLength + 2,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"S7 ScalarByteWidth called for non-buffer type {tag.DataType} (tag '{tag.Name}')"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Pure decode step — turns a raw S7 (big-endian) byte block into the wide scalar's CLR
|
||||
/// value (<c>long</c> / <c>ulong</c> / <c>double</c>), boxed as <see cref="object"/>. No
|
||||
/// network I/O — factored out of <see cref="ReadScalarBlockAsync"/> so the codec is
|
||||
/// unit-testable against a known block without a live PLC (S7.Net ships no in-process
|
||||
/// fake). Mirrors <see cref="DecodeArrayBlock"/>.
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag definition carrying the wide <see cref="S7DataType"/>.</param>
|
||||
/// <param name="addr">Parsed address (carried for the error surface + the Timer/Counter seam).</param>
|
||||
/// <param name="block">Raw contiguous byte block read from the PLC (length == <see cref="ScalarByteWidth"/>).</param>
|
||||
/// <returns>The decoded scalar value boxed as <see cref="object"/>.</returns>
|
||||
internal static object DecodeScalarBlock(S7TagDefinition tag, S7ParsedAddress addr, byte[] block)
|
||||
{
|
||||
// Timer/Counter decode added in a follow-up — a branch on addr.Area prepends here.
|
||||
// Each numeric arm is boxed to object explicitly: a bare switch expression would unify
|
||||
// long/ulong/double to their common type (double) and box THAT, mis-typing Int64/UInt64.
|
||||
return tag.DataType switch
|
||||
{
|
||||
S7DataType.Int64 => (object)System.Buffers.Binary.BinaryPrimitives.ReadInt64BigEndian(block),
|
||||
S7DataType.UInt64 => System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(block),
|
||||
S7DataType.Float64 => System.Buffers.Binary.BinaryPrimitives.ReadDoubleBigEndian(block),
|
||||
|
||||
S7DataType.String => throw new NotSupportedException(
|
||||
"S7 String scalar reads land in a follow-up PR"),
|
||||
S7DataType.DateTime => throw new NotSupportedException(
|
||||
"S7 DateTime scalar reads land in a follow-up PR"),
|
||||
|
||||
_ => throw new System.IO.InvalidDataException(
|
||||
$"S7 scalar Read type-mismatch: tag '{tag.Name}' declared {tag.DataType} but address " +
|
||||
$"'{tag.Address}' parsed as Size={addr.Size}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure encode step — turns the caller's value into the raw S7 (big-endian) byte block
|
||||
/// for a wide scalar write. No network I/O — factored out of
|
||||
/// <see cref="WriteScalarBlockAsync"/> so the codec is unit-testable (mirrors
|
||||
/// <see cref="BoxValueForWrite"/>).
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag definition carrying the wide <see cref="S7DataType"/>.</param>
|
||||
/// <param name="value">Value to encode (coerced via <c>Convert.To*</c>).</param>
|
||||
/// <returns>The big-endian byte block to write.</returns>
|
||||
internal static byte[] EncodeScalarBlock(S7TagDefinition tag, object? value)
|
||||
{
|
||||
switch (tag.DataType)
|
||||
{
|
||||
case S7DataType.Int64:
|
||||
{
|
||||
var b = new byte[8];
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteInt64BigEndian(b, Convert.ToInt64(value));
|
||||
return b;
|
||||
}
|
||||
case S7DataType.UInt64:
|
||||
{
|
||||
var b = new byte[8];
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt64BigEndian(b, Convert.ToUInt64(value));
|
||||
return b;
|
||||
}
|
||||
case S7DataType.Float64:
|
||||
{
|
||||
var b = new byte[8];
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteDoubleBigEndian(b, Convert.ToDouble(value));
|
||||
return b;
|
||||
}
|
||||
|
||||
case S7DataType.String:
|
||||
throw new NotSupportedException("S7 String scalar writes land in a follow-up PR");
|
||||
case S7DataType.DateTime:
|
||||
throw new NotSupportedException("S7 DateTime scalar writes land in a follow-up PR");
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"S7 EncodeScalarBlock called for non-buffer type {tag.DataType} (tag '{tag.Name}')");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Width in bytes of one array element for the given access size. Bit elements are
|
||||
/// byte-granular over the wire (one byte per bool), so they cost 1 byte each.</summary>
|
||||
/// <param name="size">The parsed access width.</param>
|
||||
@@ -770,6 +906,23 @@ public sealed class S7Driver
|
||||
|
||||
private async Task WriteOneAsync(Plc plc, S7TagDefinition tag, object? value, CancellationToken ct)
|
||||
{
|
||||
// Parse the address the same way ReadOneAsync does: authored tags pre-parse at init
|
||||
// (_parsedByName); an equipment-tag ref (resolved transiently) parses on demand. Needed
|
||||
// here so the wide-type write can byte-address the block (the narrow path below addresses
|
||||
// by the raw address string instead).
|
||||
var addr = _parsedByName.TryGetValue(tag.Name, out var parsed)
|
||||
? parsed
|
||||
: S7AddressParser.Parse(tag.Address);
|
||||
|
||||
// Wide/structured scalar path: encode the value to a big-endian byte block and write it
|
||||
// at the start byte via S7.Net's buffer-based WriteBytesAsync. Mirrors the read seam;
|
||||
// the narrow string-based write below stays unchanged for 1/2/4-byte types.
|
||||
if (IsBufferType(tag, addr))
|
||||
{
|
||||
await WriteScalarBlockAsync(plc, tag, addr, value, ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// S7.Net's Plc.WriteAsync(string address, object value) expects the boxed value to
|
||||
// match the address's size-suffix type: DBX=bool, DBB=byte, DBW=ushort, DBD=uint.
|
||||
// Our S7DataType lets the caller pass short/int/float; convert to the unsigned
|
||||
@@ -778,6 +931,19 @@ public sealed class S7Driver
|
||||
await plc.WriteAsync(tag.Address, boxed, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a wide/structured scalar as ONE contiguous byte block via S7.Net's
|
||||
/// buffer-based <c>Plc.WriteBytesAsync(DataType, db, startByteAdr, byte[] value, ct)</c>.
|
||||
/// The pure <see cref="EncodeScalarBlock"/> produces the big-endian bytes; this method
|
||||
/// owns only the network I/O (mirrors <see cref="ReadScalarBlockAsync"/>).
|
||||
/// </summary>
|
||||
private async Task WriteScalarBlockAsync(Plc plc, S7TagDefinition tag, S7ParsedAddress addr, object? value, CancellationToken ct)
|
||||
{
|
||||
var bytes = EncodeScalarBlock(tag, value);
|
||||
await plc.WriteBytesAsync(ToS7NetArea(addr.Area), addr.DbNumber, addr.ByteOffset, bytes, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure boxing step — converts the caller's value into the unsigned wire type that
|
||||
/// S7.Net's <c>Plc.WriteAsync</c> expects for each address size (bool → bool, byte
|
||||
|
||||
Reference in New Issue
Block a user