feat(s7): STRING/WSTRING/CHAR/WCHAR and DTL/DATE/TOD/TIME/S5TIME codecs, TSAP/connection-type override, negotiated PDU surfaced (#632 #288 #289 #294 #295)
New S7DataType members WString/Char/WChar/Dtl/Date/Tod/Time/S5Time mapped onto existing DriverDataType (String / DateTime / Int32 ms); writes refuse out-of-range with BadOutOfRange. Optional localTsap/remoteTsap/connectionType through options, factory JSON, probe and AdminUI form. NegotiatedPduSize snapshotted at connect and exposed via GetDiagnostics (DriverHealth has no bag). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -16,3 +16,20 @@ public enum S7CpuType
|
||||
S71200 = 30,
|
||||
S71500 = 40,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ISO-TSAP connection resource requested during the handshake — the FIRST byte of the remote TSAP
|
||||
/// (the second byte is <c>rack × 0x20 + slot</c>). Values are the on-wire bytes. Leave unset to keep
|
||||
/// S7.Net's per-CPU default (<see cref="S7Basic"/> for the S7-300/400/1200/1500 families).
|
||||
/// </summary>
|
||||
public enum S7ConnectionType
|
||||
{
|
||||
/// <summary>Programming-device (PG) connection resource.</summary>
|
||||
PG = 1,
|
||||
|
||||
/// <summary>Operator-panel (OP/HMI) connection resource.</summary>
|
||||
OP = 2,
|
||||
|
||||
/// <summary>"S7 basic" / other-communication connection resource — S7.Net's default.</summary>
|
||||
S7Basic = 3,
|
||||
}
|
||||
|
||||
@@ -51,6 +51,27 @@ public sealed class S7DriverOptions
|
||||
/// </summary>
|
||||
public short Slot { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Optional override of the LOCAL ISO-TSAP, as two hex bytes <c>"01.00"</c> (also accepted:
|
||||
/// <c>"0100"</c>, <c>"0x0100"</c>). Null/blank keeps S7.Net's per-CPU default (<c>01.00</c>).
|
||||
/// Needed for CPUs / gateways that reserve connection resources by TSAP (S7-400H, CP343, LOGO!).
|
||||
/// </summary>
|
||||
public string? LocalTsap { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional override of the REMOTE ISO-TSAP, same text form as <see cref="LocalTsap"/>
|
||||
/// (e.g. <c>"03.01"</c> = S7-basic resource, rack 0 slot 1). Null/blank keeps the slot-derived default.
|
||||
/// Mutually exclusive with <see cref="ConnectionType"/>.
|
||||
/// </summary>
|
||||
public string? RemoteTsap { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional connection resource (PG / OP / S7Basic) written into the first byte of the slot-derived
|
||||
/// remote TSAP, keeping <c>rack × 0x20 + slot</c> as the second byte. Null keeps the default.
|
||||
/// Mutually exclusive with <see cref="RemoteTsap"/>.
|
||||
/// </summary>
|
||||
public S7ConnectionType? ConnectionType { get; init; }
|
||||
|
||||
/// <summary>Connect + per-operation timeout.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
@@ -197,5 +218,22 @@ public enum S7DataType
|
||||
Float32,
|
||||
Float64,
|
||||
String,
|
||||
/// <summary>S7 DATE_AND_TIME (DT, 8-byte BCD) → DateTime. The other date/time members below were appended after it.</summary>
|
||||
DateTime,
|
||||
/// <summary>S7 WSTRING (UTF-16BE, 4-byte header) → String. <c>StringLength</c> is the declared max in characters (≤ 16382).</summary>
|
||||
WString,
|
||||
/// <summary>S7 CHAR (1 byte, Latin-1) → one-character String.</summary>
|
||||
Char,
|
||||
/// <summary>S7 WCHAR (UTF-16BE code unit) → one-character String.</summary>
|
||||
WChar,
|
||||
/// <summary>S7 DTL (12 bytes, ns resolution) → DateTime.</summary>
|
||||
Dtl,
|
||||
/// <summary>S7 DATE (UInt16 days since 1990-01-01) → DateTime.</summary>
|
||||
Date,
|
||||
/// <summary>S7 TIME_OF_DAY (UInt32 ms since midnight) → Int32 milliseconds.</summary>
|
||||
Tod,
|
||||
/// <summary>S7 TIME (Int32 ms, signed duration) → Int32 milliseconds.</summary>
|
||||
Time,
|
||||
/// <summary>S7 S5TIME (BCD + time base) → Int32 milliseconds.</summary>
|
||||
S5Time,
|
||||
}
|
||||
|
||||
@@ -20,6 +20,20 @@ public static class S7TagDefinitionFactory
|
||||
/// <summary>S7 string max length (DBSTRING header reserves 2 bytes; 254 chars max).</summary>
|
||||
private const int MaxStringLength = 254;
|
||||
|
||||
/// <summary>S7 WSTRING max length in characters (S7-1500; the 4-byte header's UInt16 is capped at 16382 so the field fits a DB).</summary>
|
||||
private const int MaxWStringLength = 16382;
|
||||
|
||||
/// <summary>
|
||||
/// The declared-length ceiling for a type that carries a <c>stringLength</c>, or null for a type that
|
||||
/// does not (every non-string type ignores the key, so a copy-paste remnant never blocks parsing).
|
||||
/// </summary>
|
||||
private static int? MaxLengthFor(S7DataType t) => t switch
|
||||
{
|
||||
S7DataType.String => MaxStringLength,
|
||||
S7DataType.WString => MaxWStringLength,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps an authored <c>TagConfig</c> object to a typed definition keyed by <paramref name="rawPath"/>.
|
||||
/// The input is always an authored TagConfig JSON object (there is no name-vs-blob heuristic in v3).
|
||||
@@ -55,7 +69,7 @@ public static class S7TagDefinitionFactory
|
||||
// Range-guard applies only to String tags: an S7 string can't exceed 254 chars, and a
|
||||
// negative length is meaningless. For non-String types stringLength is irrelevant and any
|
||||
// authored value (including an out-of-range remnant from copy-paste) must not block parsing.
|
||||
if (dataType == S7DataType.String && (stringLength < 0 || stringLength > MaxStringLength)) return false;
|
||||
if (MaxLengthFor(dataType) is { } maxLen && (stringLength < 0 || stringLength > maxLen)) return false;
|
||||
// Array intent: the canonical sink-side parse (DeploymentArtifact.ExtractTagArray)
|
||||
// honours arrayLength ONLY when isArray is true AND the prop is a JSON number — mirror
|
||||
// that here so the driver's def agrees byte-for-byte with the materialised OPC UA node's
|
||||
@@ -230,14 +244,14 @@ public static class S7TagDefinitionFactory
|
||||
if (TagConfigJson.DescribeInvalidEnum<S7DataType>(root, DataTypeKey) is { } w) warnings.Add(w);
|
||||
|
||||
if (TagConfigJson.TryReadEnumStrict(root, DataTypeKey, S7DataType.Int16, out var dataType)
|
||||
&& dataType == S7DataType.String)
|
||||
&& MaxLengthFor(dataType) is { } maxLen)
|
||||
{
|
||||
var stringLength = ReadInt(root, "stringLength");
|
||||
if (stringLength < 0 || stringLength > MaxStringLength)
|
||||
if (stringLength < 0 || stringLength > maxLen)
|
||||
{
|
||||
warnings.Add(
|
||||
$"S7 String tag has a 'stringLength' of {stringLength} — an S7 string is 0–"
|
||||
+ $"{MaxStringLength} characters, so the tag is rejected (BadNodeIdUnknown).");
|
||||
$"S7 {dataType} tag has a 'stringLength' of {stringLength} — an S7 {dataType} is 0–"
|
||||
+ $"{maxLen} characters, so the tag is rejected (BadNodeIdUnknown).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user