Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Tsap.cs
T
Joseph Doherty 34c52defb3 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
2026-10-08 14:31:44 -04:00

89 lines
4.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using S7.Net.Protocol;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
/// <summary>
/// ISO-TSAP helpers for the optional <c>LocalTsap</c> / <c>RemoteTsap</c> / <c>ConnectionType</c>
/// config: parsing the hex text form and resolving the effective <see cref="TsapPair"/> handed to
/// S7.Net. Pure — no I/O — so the resolution rules are unit-testable without a PLC.
/// </summary>
internal static class S7Tsap
{
/// <summary>
/// Parses a TSAP written as two hex bytes — <c>"01.00"</c>, <c>"03:01"</c> (1–2 digits per byte) — or
/// as one 16-bit hex value, <c>"0100"</c> / <c>"0x0301"</c>. High byte first.
/// </summary>
/// <param name="text">The configured text.</param>
/// <param name="tsap">The 16-bit TSAP (<c>first &lt;&lt; 8 | second</c>).</param>
/// <returns>True when <paramref name="text"/> is a valid TSAP.</returns>
internal static bool TryParse(string? text, out ushort tsap)
{
tsap = 0;
if (string.IsNullOrWhiteSpace(text)) return false;
var t = text.Trim();
var sep = t.IndexOfAny(['.', ':', '-']);
if (sep >= 0)
{
if (!TryHexByte(t[..sep], out var hi) || !TryHexByte(t[(sep + 1)..], out var lo)) return false;
tsap = (ushort)((hi << 8) | lo);
return true;
}
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t[2..];
if (t.Length is < 1 or > 4) return false;
return ushort.TryParse(t, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out tsap);
}
private static bool TryHexByte(string s, out byte b)
{
b = 0;
s = s.Trim();
return s.Length is >= 1 and <= 2
&& byte.TryParse(s, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out b);
}
/// <summary>Formats a 16-bit TSAP as canonical <c>"XX.YY"</c> (upper-case hex).</summary>
/// <param name="tsap">The 16-bit TSAP.</param>
/// <returns>The canonical text.</returns>
internal static string Format(ushort tsap) => $"{tsap >> 8:X2}.{tsap & 0xFF:X2}";
/// <summary>
/// Resolves the TSAP pair to connect with, or <c>null</c> when the options carry no override (the
/// caller then keeps the plain <c>Plc(cpu, host, port, rack, slot)</c> construction, unchanged).
/// Starts from S7.Net's own per-CPU default for <paramref name="options"/>'s rack/slot, then applies,
/// in order: <c>LocalTsap</c> replaces the local TSAP; <c>RemoteTsap</c> replaces the remote TSAP;
/// <c>ConnectionType</c> replaces only the remote TSAP's first byte.
/// </summary>
/// <param name="options">The driver options.</param>
/// <returns>The effective pair, or null for "no override".</returns>
internal static TsapPair? Resolve(S7DriverOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var hasLocal = !string.IsNullOrWhiteSpace(options.LocalTsap);
var hasRemote = !string.IsNullOrWhiteSpace(options.RemoteTsap);
if (!hasLocal && !hasRemote && options.ConnectionType is null) return null;
var def = TsapPair.GetDefaultTsapPair(S7CpuTypeMap.ToS7Net(options.CpuType), options.Rack, options.Slot);
var local = def.Local;
var remote = def.Remote;
if (hasLocal)
{
if (!TryParse(options.LocalTsap, out var l))
throw new FormatException($"S7 LocalTsap '{options.LocalTsap}' is not a valid TSAP (expected e.g. \"01.00\").");
local = new Tsap((byte)(l >> 8), (byte)(l & 0xFF));
}
if (hasRemote)
{
if (!TryParse(options.RemoteTsap, out var r))
throw new FormatException($"S7 RemoteTsap '{options.RemoteTsap}' is not a valid TSAP (expected e.g. \"03.01\").");
remote = new Tsap((byte)(r >> 8), (byte)(r & 0xFF));
}
else if (options.ConnectionType is { } ct)
{
remote = new Tsap((byte)ct, remote.SecondByte);
}
return new TsapPair(local, remote);
}
}