Auto: s7-a3 — DTL/DT/S5TIME/TIME/TOD/DATE codecs
Adds S7DateTimeCodec static class implementing the six Siemens S7 date/time wire formats: - DTL (12 bytes): UInt16 BE year + month/day/dow/h/m/s + UInt32 BE nanos - DATE_AND_TIME (8 bytes BCD): yy/mm/dd/hh/mm/ss + 3-digit BCD ms + dow - S5TIME (16 bits): 2-bit timebase + 3-digit BCD count → TimeSpan - TIME (Int32 ms BE, signed) → TimeSpan, allows negative durations - TOD (UInt32 ms BE, 0..86399999) → TimeSpan since midnight - DATE (UInt16 BE days since 1990-01-01) → DateTime Mirrors the S7StringCodec pattern from PR-S7-A2 — codecs operate on raw byte spans so each format can be locked with golden-byte unit tests without a live PLC. New S7DataType members (Dtl, DateAndTime, S5Time, Time, TimeOfDay, Date) are wired into S7Driver.ReadOneAsync/WriteOneAsync via byte-level ReadBytesAsync/WriteBytesAsync calls — S7.Net's string-keyed Read/Write overloads have no syntax for these widths. Uninitialized PLC buffers (all-zero year+month for DTL/DT) reject as InvalidDataException → BadOutOfRange to operators, rather than decoding as year-0001 garbage. S5TIME / TIME / TOD surface as Int32 ms (DriverDataType has no Duration); DTL / DT / DATE surface as DriverDataType.DateTime. Test coverage: 30 new golden-vector + round-trip + rejection tests, including the all-zero buffer rejection paths and BCD-nibble validation. Build clean, 115/115 S7 tests pass. Closes #289 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,48 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
||||
}
|
||||
}
|
||||
|
||||
// Date/time-shaped types (DTL/DT/S5TIME/TIME/TOD/DATE): S7.Net has no native size
|
||||
// suffix for any of these, so the driver issues a raw byte read at the address's
|
||||
// ByteOffset and decodes via S7DateTimeCodec. All require byte-addressing — bit-
|
||||
// access against a date/time tag is a config bug worth surfacing as a hard error.
|
||||
if (tag.DataType is S7DataType.Dtl or S7DataType.DateAndTime or S7DataType.S5Time
|
||||
or S7DataType.Time or S7DataType.TimeOfDay or S7DataType.Date)
|
||||
{
|
||||
if (addr.Size == S7Size.Bit)
|
||||
throw new System.IO.InvalidDataException(
|
||||
$"S7 Read type-mismatch: tag '{tag.Name}' declared {tag.DataType} but address '{tag.Address}' " +
|
||||
$"parsed as bit-access; date/time types require byte-addressing");
|
||||
|
||||
int size = tag.DataType switch
|
||||
{
|
||||
S7DataType.Dtl => S7DateTimeCodec.DtlSize,
|
||||
S7DataType.DateAndTime => S7DateTimeCodec.DtSize,
|
||||
S7DataType.S5Time => S7DateTimeCodec.S5TimeSize,
|
||||
S7DataType.Time => S7DateTimeCodec.TimeSize,
|
||||
S7DataType.TimeOfDay => S7DateTimeCodec.TodSize,
|
||||
S7DataType.Date => S7DateTimeCodec.DateSize,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
var b = await plc.ReadBytesAsync(MapArea(addr.Area), addr.DbNumber, addr.ByteOffset, size, ct).ConfigureAwait(false);
|
||||
if (b is null || b.Length != size)
|
||||
throw new System.IO.InvalidDataException(
|
||||
$"S7.Net returned {b?.Length ?? 0} bytes for {tag.DataType} '{tag.Address}', expected {size}");
|
||||
|
||||
return tag.DataType switch
|
||||
{
|
||||
S7DataType.Dtl => S7DateTimeCodec.DecodeDtl(b),
|
||||
S7DataType.DateAndTime => S7DateTimeCodec.DecodeDt(b),
|
||||
// S5TIME/TIME/TOD surface as Int32 ms — DriverDataType has no Duration type;
|
||||
// OPC UA clients see a millisecond integer matching the IEC-1131 convention.
|
||||
S7DataType.S5Time => (int)S7DateTimeCodec.DecodeS5Time(b).TotalMilliseconds,
|
||||
S7DataType.Time => (int)S7DateTimeCodec.DecodeTime(b).TotalMilliseconds,
|
||||
S7DataType.TimeOfDay => (int)S7DateTimeCodec.DecodeTod(b).TotalMilliseconds,
|
||||
S7DataType.Date => S7DateTimeCodec.DecodeDate(b),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
}
|
||||
|
||||
// 64-bit types: S7.Net's string-based ReadAsync has no LWord size suffix, so issue an
|
||||
// 8-byte ReadBytesAsync and convert big-endian in-process. Wire order on S7 is BE.
|
||||
if (tag.DataType is S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64)
|
||||
@@ -402,6 +444,34 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
||||
return;
|
||||
}
|
||||
|
||||
// Date/time-shaped types: encode via S7DateTimeCodec and push as raw bytes. S5TIME /
|
||||
// TIME / TOD accept an integer-ms input (matching the read surface); DTL / DT / DATE
|
||||
// accept a DateTime. ArgumentException from the codec bubbles to BadInternalError.
|
||||
if (tag.DataType is S7DataType.Dtl or S7DataType.DateAndTime or S7DataType.S5Time
|
||||
or S7DataType.Time or S7DataType.TimeOfDay or S7DataType.Date)
|
||||
{
|
||||
var addr = _parsedByName[tag.Name];
|
||||
if (addr.Size == S7Size.Bit)
|
||||
throw new InvalidOperationException(
|
||||
$"S7 Write type-mismatch: tag '{tag.Name}' declared {tag.DataType} but address '{tag.Address}' " +
|
||||
$"parsed as bit-access; date/time types require byte-addressing");
|
||||
if (value is null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
byte[] payload = tag.DataType switch
|
||||
{
|
||||
S7DataType.Dtl => S7DateTimeCodec.EncodeDtl(Convert.ToDateTime(value)),
|
||||
S7DataType.DateAndTime => S7DateTimeCodec.EncodeDt(Convert.ToDateTime(value)),
|
||||
S7DataType.S5Time => S7DateTimeCodec.EncodeS5Time(value is TimeSpan ts1 ? ts1 : TimeSpan.FromMilliseconds(Convert.ToInt32(value))),
|
||||
S7DataType.Time => S7DateTimeCodec.EncodeTime(value is TimeSpan ts2 ? ts2 : TimeSpan.FromMilliseconds(Convert.ToInt32(value))),
|
||||
S7DataType.TimeOfDay => S7DateTimeCodec.EncodeTod(value is TimeSpan ts3 ? ts3 : TimeSpan.FromMilliseconds(Convert.ToInt64(value))),
|
||||
S7DataType.Date => S7DateTimeCodec.EncodeDate(Convert.ToDateTime(value)),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
await plc.WriteBytesAsync(MapArea(addr.Area), addr.DbNumber, addr.ByteOffset, payload, ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 64-bit types: S7.Net has no LWord-aware WriteAsync(string, object) overload, so emit
|
||||
// the value as 8 big-endian bytes via WriteBytesAsync. Wire order on S7 is BE so a
|
||||
// BinaryPrimitives.Write*BigEndian round-trips with the matching ReadOneAsync path.
|
||||
@@ -491,6 +561,14 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
||||
S7DataType.Char => DriverDataType.String,
|
||||
S7DataType.WChar => DriverDataType.String,
|
||||
S7DataType.DateTime => DriverDataType.DateTime,
|
||||
S7DataType.Dtl => DriverDataType.DateTime,
|
||||
S7DataType.DateAndTime => DriverDataType.DateTime,
|
||||
S7DataType.Date => DriverDataType.DateTime,
|
||||
// S5TIME/TIME/TOD have no Duration type in DriverDataType — surface as Int32 ms
|
||||
// (matching the IEC-1131 representation).
|
||||
S7DataType.S5Time => DriverDataType.Int32,
|
||||
S7DataType.Time => DriverDataType.Int32,
|
||||
S7DataType.TimeOfDay => DriverDataType.Int32,
|
||||
_ => DriverDataType.Int32,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user