34c52defb3
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
89 lines
4.0 KiB
C#
89 lines
4.0 KiB
C#
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 << 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);
|
||
}
|
||
}
|