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:
Joseph Doherty
2026-10-08 14:31:44 -04:00
parent d898262c4a
commit 34c52defb3
18 changed files with 1430 additions and 31 deletions
@@ -30,6 +30,32 @@
<div class="form-text">Controls ISO-TSAP slot byte during handshake.</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label" for="s7ConnType">Connection type</label>
<InputSelect id="s7ConnType" @bind-Value="_form.ConnectionTypeText" @bind-Value:after="EmitAsync"
class="form-select form-select-sm">
<option value="">Default (slot-derived)</option>
@foreach (var v in Enum.GetValues<S7ConnectionType>())
{
<option value="@v">@v</option>
}
</InputSelect>
<div class="form-text">First byte of the remote TSAP. Leave on default unless the CPU needs a PG/OP resource.</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="s7LocalTsap">Local TSAP</label>
<InputText id="s7LocalTsap" @bind-Value="_form.LocalTsap" @bind-Value:after="EmitAsync"
class="form-control form-control-sm mono" placeholder="01.00" />
<div class="form-text">Optional hex override, e.g. <code>01.00</code>.</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="s7RemoteTsap">Remote TSAP</label>
<InputText id="s7RemoteTsap" @bind-Value="_form.RemoteTsap" @bind-Value:after="EmitAsync"
class="form-control form-control-sm mono" placeholder="03.01" />
<div class="form-text">Optional hex override, e.g. <code>03.01</code>. Not combinable with Connection type.</div>
</div>
</div>
</div>
</section>
@@ -141,6 +167,10 @@
public S7CpuType CpuType { get; set; } = S7CpuType.S71500;
public short Rack { get; set; } = 0;
public short Slot { get; set; } = 0;
public string? LocalTsap { get; set; }
public string? RemoteTsap { get; set; }
// Bound to a <select> as text ("" = default) so the nullable enum round-trips through InputSelect.
public string ConnectionTypeText { get; set; } = "";
public int TimeoutSeconds { get; set; } = 5;
// Probe
@@ -160,6 +190,9 @@
CpuType = o.CpuType,
Rack = o.Rack,
Slot = o.Slot,
LocalTsap = o.LocalTsap,
RemoteTsap = o.RemoteTsap,
ConnectionTypeText = o.ConnectionType?.ToString() ?? "",
TimeoutSeconds = (int)o.Timeout.TotalSeconds,
ProbeEnabled = o.Probe.Enabled,
ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
@@ -174,6 +207,9 @@
CpuType = CpuType,
Rack = Rack,
Slot = Slot,
LocalTsap = string.IsNullOrWhiteSpace(LocalTsap) ? null : LocalTsap.Trim(),
RemoteTsap = string.IsNullOrWhiteSpace(RemoteTsap) ? null : RemoteTsap.Trim(),
ConnectionType = Enum.TryParse<S7ConnectionType>(ConnectionTypeText, out var ct) ? ct : null,
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
Probe = new S7ProbeOptions
{
@@ -120,7 +120,9 @@ public sealed class S7TagConfigModel
{
var dt = DataType;
if (dt is S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64
or S7DataType.String or S7DataType.DateTime)
or S7DataType.String or S7DataType.DateTime or S7DataType.WString or S7DataType.Char
or S7DataType.WChar or S7DataType.Dtl or S7DataType.Date or S7DataType.Tod
or S7DataType.Time or S7DataType.S5Time)
{
return $"S7 does not support {dt} arrays — use Bool (read-only), Byte, Int16, UInt16, Int32, "
+ "UInt32 or Float32 for an array tag, or a scalar tag.";