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;
}
}