using System.Buffers.Binary; using System.Collections.Concurrent; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests.Simulator; public enum SimReplyKind { Frame, Drop, Stall } /// What the simulated control does with one request. public sealed record SimReply(SimReplyKind Kind, string? Command = null, byte[]? Payload = null) { public static SimReply Ok(string command, byte[] payload) => new(SimReplyKind.Frame, command, payload); public static SimReply Refuse(int code, int type = 1) => new(SimReplyKind.Frame, "T_ER", [(byte)type, (byte)code]); public static SimReply Drop() => new(SimReplyKind.Drop); public static SimReply Stall() => new(SimReplyKind.Stall); /// Parses a reference fixture value S_RI:0004 / T_ER:0117. public static SimReply FromFixture(string value) { var parts = value.Split(':', 2); return Ok(parts[0], Convert.FromHexString(parts[1])); } } /// /// A small, mutable model of an iTNC 530 speaking the six allowlisted LSV2 requests. Anything else is /// recorded as a violation and refused — the driver must never produce one. /// public sealed class SimulatedControl { private readonly ConcurrentQueue _violations = new(); public byte[] Version { get; set; } = Encoding.Latin1.GetBytes("iTNC 530\0NC 340490 04\0PLC\0OPT\0"); public byte[]? Layout { get; set; } = LayoutBytes(10); public Func? Login { get; set; } public ConcurrentDictionary> Runtime { get; } = new(); public ConcurrentDictionary DataPaths { get; } = new(StringComparer.Ordinal); public Func? Memory { get; set; } public List<(string Role, string Password)> Logins { get; } = []; /// Requests the control refused as not-allowlisted (must stay empty). public IReadOnlyList Violations => _violations.ToArray(); /// Optional hook to override any reply (fault injection). public Func? Intercept { get; set; } public SimReply Handle(Lsv2Request req) { if (Intercept?.Invoke(req) is { } forced) return forced; switch (req.Command) { case "A_LG": { var fields = req.Text.Split('\0'); var role = fields[0]; var password = fields.Length > 2 ? fields[1] : ""; lock (Logins) Logins.Add((role, password)); return Login?.Invoke(role, password) ?? SimReply.Ok("T_OK", []); } case "R_VR" when req.Payload.Length == 0: return SimReply.Ok("S_VR", Version); case "R_PR" when req.Payload.Length == 0: return Layout is null ? SimReply.Refuse(24) : SimReply.Ok("S_PR", Layout); case "R_RI" when req.Payload.Length == 2: return Runtime.TryGetValue(req.Selector, out var f) ? f() : SimReply.Refuse(24); case "R_MB" when req.Payload.Length == 5: var address = BinaryPrimitives.ReadUInt32BigEndian(req.Payload); return Memory?.Invoke(address, req.Payload[4]) ?? SimReply.Refuse(23); case "R_DP" when req.Payload.Length > 5: { var path = Encoding.Latin1.GetString(req.Payload, 4, req.Payload.Length - 5).Replace('\\', '/'); return DataPaths.TryGetValue(path, out var r) ? r : SimReply.Refuse(24); } default: _violations.Enqueue(req.Command); return SimReply.Refuse(22); } } /// The reference test layout: words at 1000 with the given byte count, strings at 2000 (width 16), IW 3000, OW 4000. public static byte[] LayoutBytes(int wordBytes) { var b = new byte[120]; BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(40), 1000); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(44), wordBytes); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(48), 2000); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(52), 10); b[56] = 16; BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(64), 3000); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(68), 10); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(72), 4000); BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(76), 10); return b; } /// Two-byte uint16be runtime reply. public static SimReply U16(int v) { var p = new byte[2]; BinaryPrimitives.WriteUInt16BigEndian(p, (ushort)v); return SimReply.Ok("S_RI", p); } } /// Loads the ported Java .properties golden vectors. public static class Fixtures { public static IReadOnlyDictionary Load(string name) { var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", name); var map = new Dictionary(StringComparer.Ordinal); foreach (var line in File.ReadAllLines(path)) { if (line.Length == 0 || line[0] == '#') continue; var eq = line.IndexOf('='); map[line[..eq]] = line[(eq + 1)..]; } return map; } }