using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MessagePack; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Backend; /// /// In-memory for tests + an operational stub mode when /// OTOPCUA_FOCAS_BACKEND=fake. Keeps per-address values keyed by a canonical /// string; RMW semantics honor PMC bit-writes against the containing byte so the /// PmcBitWriteRequest path can be exercised end-to-end without hardware. /// public sealed class FakeFocasBackend : IFocasBackend { private readonly object _gate = new(); private long _nextSessionId; private readonly HashSet _openSessions = []; private readonly Dictionary _pmcValues = []; private readonly Dictionary _paramValues = []; private readonly Dictionary _macroValues = []; public Task OpenSessionAsync(OpenSessionRequest request, CancellationToken ct) { lock (_gate) { var id = ++_nextSessionId; _openSessions.Add(id); return Task.FromResult(new OpenSessionResponse { Success = true, SessionId = id }); } } public Task CloseSessionAsync(CloseSessionRequest request, CancellationToken ct) { lock (_gate) { _openSessions.Remove(request.SessionId); } return Task.CompletedTask; } public Task ReadAsync(ReadRequest request, CancellationToken ct) { lock (_gate) { if (!_openSessions.Contains(request.SessionId)) return Task.FromResult(new ReadResponse { Success = false, StatusCode = 0x80020000u, Error = "session-not-open" }); var store = StoreFor(request.Address.Kind); var key = CanonicalKey(request.Address); store.TryGetValue(key, out var value); return Task.FromResult(new ReadResponse { Success = true, StatusCode = 0, ValueBytes = value ?? MessagePackSerializer.Serialize((int)0), ValueTypeCode = request.DataType, SourceTimestampUtcUnixMs = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }); } } public Task WriteAsync(WriteRequest request, CancellationToken ct) { lock (_gate) { if (!_openSessions.Contains(request.SessionId)) return Task.FromResult(new WriteResponse { Success = false, StatusCode = 0x80020000u, Error = "session-not-open" }); var store = StoreFor(request.Address.Kind); store[CanonicalKey(request.Address)] = request.ValueBytes ?? []; return Task.FromResult(new WriteResponse { Success = true, StatusCode = 0 }); } } public Task PmcBitWriteAsync(PmcBitWriteRequest request, CancellationToken ct) { lock (_gate) { if (!_openSessions.Contains(request.SessionId)) return Task.FromResult(new PmcBitWriteResponse { Success = false, StatusCode = 0x80020000u, Error = "session-not-open" }); if (request.BitIndex is < 0 or > 7) return Task.FromResult(new PmcBitWriteResponse { Success = false, StatusCode = 0x803C0000u, Error = "bit-out-of-range" }); var key = CanonicalKey(request.Address); _pmcValues.TryGetValue(key, out var current); current ??= MessagePackSerializer.Serialize((byte)0); var b = MessagePackSerializer.Deserialize(current); var mask = (byte)(1 << request.BitIndex); b = request.Value ? (byte)(b | mask) : (byte)(b & ~mask); _pmcValues[key] = MessagePackSerializer.Serialize(b); return Task.FromResult(new PmcBitWriteResponse { Success = true, StatusCode = 0 }); } } public Task ProbeAsync(ProbeRequest request, CancellationToken ct) { lock (_gate) { return Task.FromResult(new ProbeResponse { Healthy = _openSessions.Contains(request.SessionId), ObservedAtUtcUnixMs = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }); } } private Dictionary StoreFor(int kind) => kind switch { 0 => _pmcValues, 1 => _paramValues, 2 => _macroValues, _ => _pmcValues, }; private static string CanonicalKey(FocasAddressDto addr) => addr.Kind switch { 0 => $"{addr.PmcLetter}{addr.Number}", 1 => $"P{addr.Number}", 2 => $"M{addr.Number}", _ => $"?{addr.Number}", }; }