123 lines
4.7 KiB
C#
123 lines
4.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// In-memory <see cref="IFocasBackend"/> for tests + an operational stub mode when
|
|
/// <c>OTOPCUA_FOCAS_BACKEND=fake</c>. Keeps per-address values keyed by a canonical
|
|
/// string; RMW semantics honor PMC bit-writes against the containing byte so the
|
|
/// <c>PmcBitWriteRequest</c> path can be exercised end-to-end without hardware.
|
|
/// </summary>
|
|
public sealed class FakeFocasBackend : IFocasBackend
|
|
{
|
|
private readonly object _gate = new();
|
|
private long _nextSessionId;
|
|
private readonly HashSet<long> _openSessions = [];
|
|
private readonly Dictionary<string, byte[]> _pmcValues = [];
|
|
private readonly Dictionary<string, byte[]> _paramValues = [];
|
|
private readonly Dictionary<string, byte[]> _macroValues = [];
|
|
|
|
public Task<OpenSessionResponse> 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<ReadResponse> 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<WriteResponse> 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<PmcBitWriteResponse> 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<byte>(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<ProbeResponse> ProbeAsync(ProbeRequest request, CancellationToken ct)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return Task.FromResult(new ProbeResponse
|
|
{
|
|
Healthy = _openSessions.Contains(request.SessionId),
|
|
ObservedAtUtcUnixMs = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
});
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, byte[]> 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}",
|
|
};
|
|
}
|