ba9015fac8
- HeidenhainControlFamily + HeidenhainControlFamilies.Classify (Contracts): one authority, ported from pyLSV2 (MIT) dat_cls.py VersionInfo.control (strip spaces, case-insensitive prefix, ITNC530 first). Families: ITnc530, Tnc (640/620/320/128), Tnc7, Lathe (CNC PILOT 640 / MANUALplus 620), MillPlus, LegacyTnc (426/430, not a pyLSV2 class), Unknown. - New additive read-only leaf Controller/Family (String, Identity feature, derived from the same R_VR reply: no extra request). Lsv2Versions.Family is computed, so record equality and the #831 identity-change rediscovery signal are unchanged. Probe message + /hosts diagnostics (<host>.family). - DATA gate now goes through SupportsDataPath(family) (pyLSV2 client.py read_data_path is iTNC-only): non-iTNC and Unknown => Capabilities/DataLogin Unsupported, DATA login never sent. Replaces the "model contains itnc" substring check (same result on the real iTNC530 controls). - No per-family selector pre-gates: pyLSV2's TNC 640 observations (27/28/51/30-34 unknown telegram, 25/26 need DNC) are already handled by the reply-driven refusal/suppression path; tests pin it. - Lsv2ReadOnlyPolicy, HeidenhainTagWritable and the no-IWritable rule are untouched. - Simulator: per-family ControlProfiles (strings from the pyLSV2 README tables / protocol.rst), both R_VR field counts, 124-byte R_PR; HeidenhainControlFamilyTests (classification, gates, decoding, wire log inside the read-only set, refused selectors asked once). - docs/drivers/Heidenhain.md: Control families matrix (iTNC 530 hardware-proven; the rest per pyLSV2, hardware-unproven) + open questions (C_CC buffer size, encrypted passwords, R_RI 20/21). Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
160 lines
8.5 KiB
C#
160 lines
8.5 KiB
C#
using System.Buffers.Binary;
|
||
using System.Text;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests.Simulator;
|
||
|
||
/// <summary>How a simulated family answers the <c>R_RI</c> selectors the driver reads.</summary>
|
||
public enum SimSelectorBehaviour
|
||
{
|
||
/// <summary>Every read selector answers (the iTNC 530 shape, hardware-captured).</summary>
|
||
All,
|
||
|
||
/// <summary>
|
||
/// The TNC 640 observation (pyLSV2 docs/protocol.rst "Differences between different control versions",
|
||
/// NC 340590 10 SP2): 27/28/51 and 30–34 refuse <c>T_ER_UNKNOWN_TELE</c> (22); 25/26 need DNC; 24 serves the
|
||
/// program without DNC but with block number 0.
|
||
/// </summary>
|
||
TncObserved,
|
||
|
||
/// <summary>
|
||
/// Core telemetry only (22–26). pyLSV2's own suite asserts overrides/axes/exec state on every control but gates
|
||
/// tool (51) and error messages (27/28) to iTNC (tests/test_read_info.py), and its protocol table marks them
|
||
/// "no" on CNCpilot — so 27/28/51 and the iTNC lifetime timers 30–34 are refused 22 here. A conservative
|
||
/// assumption, not an observation, for the families pyLSV2 records nothing about.
|
||
/// </summary>
|
||
CoreOnly,
|
||
}
|
||
|
||
/// <summary>One simulated HEIDENHAIN control family.</summary>
|
||
/// <param name="Name">Test display name.</param>
|
||
/// <param name="Model">The R_VR model field.</param>
|
||
/// <param name="Nc">The R_VR NC software field.</param>
|
||
/// <param name="Family">The family the driver must classify it as.</param>
|
||
/// <param name="Selectors">Which R_RI selectors answer.</param>
|
||
/// <param name="FiveFieldVersion">True to send the 5-field R_VR (model, NC, PLC, SPLC, options).</param>
|
||
/// <param name="Source">Where the profile's strings come from.</param>
|
||
public sealed record ControlProfile(
|
||
string Name, string Model, string Nc, HeidenhainControlFamily Family, SimSelectorBehaviour Selectors,
|
||
bool FiveFieldVersion, string Source)
|
||
{
|
||
public override string ToString() => Name;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Per-family simulator profiles. Model/NC strings come from the pyLSV2 (MIT) README "Programming Stations" and
|
||
/// "Machines" tables and dat_cls.py:VersionInfo; the iTNC 530 one from our own hardware record
|
||
/// (docs/drivers/evidence/heidenhain-live-2026-11-11.md). The R_VR field count follows pyLSV2 (MIT)
|
||
/// client.py:_read_version, which accepts exactly 4 or 5 NUL-separated fields on every family — which family
|
||
/// sends which shape is not recorded, so the 5-field variant is exercised on more than one family.
|
||
/// </summary>
|
||
public static class ControlProfiles
|
||
{
|
||
public static readonly ControlProfile ITnc530 = new("iTNC 530", "iTNC530", "340490 04 SP10",
|
||
HeidenhainControlFamily.ITnc530, SimSelectorBehaviour.All, false, "hardware: Hermle59 2026-11-11");
|
||
|
||
public static readonly ControlProfile ITnc530Spaced = new("iTNC 530 (spaced)", "iTNC 530", "606420 03 SP4",
|
||
HeidenhainControlFamily.ITnc530, SimSelectorBehaviour.All, true, "pyLSV2 dat_cls.py strips spaces before classifying");
|
||
|
||
public static readonly ControlProfile Tnc640 = new("TNC 640", "TNC640", "340590 10 SP2",
|
||
HeidenhainControlFamily.Tnc, SimSelectorBehaviour.TncObserved, false, "pyLSV2 docs/protocol.rst + README");
|
||
|
||
public static readonly ControlProfile Tnc620 = new("TNC 620", "TNC620", "817605 04 SP1",
|
||
HeidenhainControlFamily.Tnc, SimSelectorBehaviour.TncObserved, true, "pyLSV2 README Machines");
|
||
|
||
public static readonly ControlProfile Tnc320 = new("TNC 320", "TNC320", "340554 04 SP1",
|
||
HeidenhainControlFamily.Tnc, SimSelectorBehaviour.TncObserved, false, "pyLSV2 README Programming Stations");
|
||
|
||
public static readonly ControlProfile Tnc128 = new("TNC 128", "TNC 128", "771841 01",
|
||
HeidenhainControlFamily.Tnc, SimSelectorBehaviour.TncObserved, false, "pyLSV2 dat_cls.py prefix only (NC string illustrative)");
|
||
|
||
public static readonly ControlProfile Tnc7 = new("TNC7", "TNC7", "817625 17",
|
||
HeidenhainControlFamily.Tnc7, SimSelectorBehaviour.CoreOnly, true, "pyLSV2 README Programming Stations");
|
||
|
||
public static readonly ControlProfile CncPilot640 = new("CNC PILOT 640", "CNCpilot640", "1230521 03 SP1",
|
||
HeidenhainControlFamily.Lathe, SimSelectorBehaviour.CoreOnly, false, "pyLSV2 README Programming Stations");
|
||
|
||
public static readonly ControlProfile ManualPlus620 = new("MANUALplus 620", "MANUALplus620", "634130 02 SP7",
|
||
HeidenhainControlFamily.Lathe, SimSelectorBehaviour.CoreOnly, false, "pyLSV2 README Programming Stations");
|
||
|
||
public static readonly ControlProfile MillPlus = new("MillPlus IT V600", "MillPlusIT V600", "538956 03 SP15",
|
||
HeidenhainControlFamily.MillPlus, SimSelectorBehaviour.CoreOnly, false, "pyLSV2 README Programming Stations");
|
||
|
||
public static readonly ControlProfile Tnc430 = new("TNC 430 (legacy)", "TNC 430", "280476 21",
|
||
HeidenhainControlFamily.LegacyTnc, SimSelectorBehaviour.CoreOnly, false, "no pyLSV2 rule (it reports UNKNOWN); illustrative");
|
||
|
||
public static readonly ControlProfile Unrecognised = new("Unrecognised", "Anilam 6000i", "000000 00",
|
||
HeidenhainControlFamily.Unknown, SimSelectorBehaviour.CoreOnly, false, "pyLSV2 ControlType.UNKNOWN");
|
||
|
||
public static IReadOnlyList<ControlProfile> All { get; } =
|
||
[ITnc530, ITnc530Spaced, Tnc640, Tnc620, Tnc320, Tnc128, Tnc7, CncPilot640, ManualPlus620, MillPlus, Tnc430, Unrecognised];
|
||
|
||
/// <summary>The R_VR reply for a profile.</summary>
|
||
public static byte[] VersionReply(ControlProfile p) => Encoding.Latin1.GetBytes(p.FiveFieldVersion
|
||
? $"{p.Model}\0{p.Nc}\0PLC1\0SPLC1\0%0000111100000111\0"
|
||
: $"{p.Model}\0{p.Nc}\0PLC1\0%0000111100000111\0");
|
||
|
||
/// <summary>A simulated control speaking the profile. <c>DataPaths["/x"]</c> is populated for every family, so a gate — not the simulator — is what keeps non-iTNC DATA off the wire.</summary>
|
||
public static SimulatedControl Build(ControlProfile p)
|
||
{
|
||
var dnc = 0;
|
||
var c = new SimulatedControl
|
||
{
|
||
Version = VersionReply(p),
|
||
Login = (role, _) =>
|
||
{
|
||
if (role == "DNC") Interlocked.Exchange(ref dnc, 1);
|
||
return SimReply.Ok("T_OK", []);
|
||
},
|
||
};
|
||
bool Dnc() => Volatile.Read(ref dnc) == 1;
|
||
|
||
c.Runtime[22] = () => SimReply.Ok("S_RI", [0, 2, .. Encoding.ASCII.GetBytes("1.5\0-2.25\0X\0Z\0")]);
|
||
c.Runtime[23] = () => SimulatedControl.U16(4);
|
||
c.DataPaths["/x"] = SimReply.Ok("S_DP", [0, 0, 0, 3, 0, 0, 0, 42]);
|
||
switch (p.Selectors)
|
||
{
|
||
case SimSelectorBehaviour.TncObserved:
|
||
c.Runtime[24] = () => SimReply.Ok("S_RI", Program(Dnc() ? 5 : 0));
|
||
c.Runtime[25] = () => Dnc() ? SimReply.Ok("S_RI", Overrides()) : SimReply.Refuse(23);
|
||
c.Runtime[26] = () => Dnc() ? SimulatedControl.U16(7) : SimReply.Refuse(23);
|
||
foreach (var s in new[] { 27, 28, 51, 30, 31, 32, 33, 34 }) c.Runtime[s] = () => SimReply.Refuse(22);
|
||
break;
|
||
case SimSelectorBehaviour.CoreOnly:
|
||
c.Runtime[24] = () => SimReply.Ok("S_RI", Program(5));
|
||
c.Runtime[25] = () => SimReply.Ok("S_RI", Overrides());
|
||
c.Runtime[26] = () => SimulatedControl.U16(7);
|
||
foreach (var s in new[] { 27, 28, 51, 30, 31, 32, 33, 34 }) c.Runtime[s] = () => SimReply.Refuse(22);
|
||
break;
|
||
default:
|
||
c.Runtime[24] = () => SimReply.Ok("S_RI", Program(5));
|
||
c.Runtime[25] = () => SimReply.Ok("S_RI", Overrides());
|
||
c.Runtime[26] = () => SimulatedControl.U16(7);
|
||
c.Runtime[27] = () => SimReply.Ok("S_RI", Alarm());
|
||
c.Runtime[28] = () => SimReply.Refuse(57);
|
||
c.Runtime[51] = () => SimReply.Ok("S_RI", new byte[24]);
|
||
for (var s = 30; s <= 34; s++) c.Runtime[s] = () => SimReply.Ok("S_RI", [0, 0, 1, 0]);
|
||
break;
|
||
}
|
||
|
||
return c;
|
||
}
|
||
|
||
private static byte[] Program(int line)
|
||
{
|
||
var b = new byte[4];
|
||
BinaryPrimitives.WriteUInt32BigEndian(b, (uint)line);
|
||
return [.. b, .. "TNC:\\nc_prog\\a.h\0TNC:\\nc_prog\\a.h\0"u8.ToArray()];
|
||
}
|
||
|
||
private static byte[] Overrides()
|
||
{
|
||
var b = new byte[12];
|
||
BinaryPrimitives.WriteUInt32BigEndian(b, 10000);
|
||
BinaryPrimitives.WriteUInt32BigEndian(b.AsSpan(4), 9000);
|
||
BinaryPrimitives.WriteUInt32BigEndian(b.AsSpan(8), 5000);
|
||
return b;
|
||
}
|
||
|
||
private static byte[] Alarm() => [0, 7, 0, 4, 0xff, 0xff, 0xff, 0xff, .. "Run test of cutout channels\0"u8.ToArray()];
|
||
}
|