using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; internal class FakeFocasClient : IFocasClient { /// Gets a value indicating whether the client is connected. public bool IsConnected { get; private set; } /// Gets the count of connection attempts. public int ConnectCount { get; private set; } /// Gets the count of dispose operations. public int DisposeCount { get; private set; } /// Gets or sets a value indicating whether to throw on connect. public bool ThrowOnConnect { get; set; } /// Gets or sets a value indicating whether to throw on read. public bool ThrowOnRead { get; set; } /// Gets or sets a value indicating whether to throw on write. public bool ThrowOnWrite { get; set; } /// Gets or sets the result of probe operations. public bool ProbeResult { get; set; } = true; /// Gets or sets the exception to throw. public Exception? Exception { get; set; } /// Gets the dictionary of read values keyed by address. public Dictionary Values { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the dictionary of read statuses keyed by address. public Dictionary ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the dictionary of write statuses keyed by address. public Dictionary WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the log of write operations. public List<(FocasAddress addr, FocasDataType type, object? value)> WriteLog { get; } = new(); /// Connects to a FOCAS host asynchronously. /// The FOCAS host address. /// The connection timeout duration. /// The cancellation token. public virtual Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken ct) { ConnectCount++; if (ThrowOnConnect) throw Exception ?? new InvalidOperationException(); IsConnected = true; return Task.CompletedTask; } /// Reads a value from a FOCAS address asynchronously. /// The FOCAS address to read from. /// The data type of the value. /// The cancellation token. public virtual Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, CancellationToken ct) { if (ThrowOnRead) throw Exception ?? new InvalidOperationException(); var key = address.Canonical; var status = ReadStatuses.TryGetValue(key, out var s) ? s : FocasStatusMapper.Good; var value = Values.TryGetValue(key, out var v) ? v : null; return Task.FromResult((value, status)); } /// PMC byte memory the range read serves, keyed by (upper-case letter, byte address); absent = 0. public Dictionary<(string Letter, int Address), byte> PmcMemory { get; } = new(); /// Gets the log of range reads issued (letter, slot type, start, end). public List<(string Letter, FocasDataType SlotType, int Start, int End)> PmcRangeReads { get; } = new(); /// Ranges (letter, start, end) the fake CNC rejects with . public List> PmcRangeRejectors { get; } = new(); /// The status a rejected range returns. public uint PmcRangeRejectStatus { get; set; } = FocasStatusMapper.BadOutOfRange; /// When true every range read throws (transport failure). public bool ThrowOnPmcRange { get; set; } /// Reads a PMC range as raw big-endian slots from . /// PMC letter. /// Slot type. /// First byte. /// Last byte, inclusive. /// The cancellation token. public virtual Task<(IReadOnlyList? slots, uint status)> ReadPmcSlotsAsync( string letter, FocasDataType slotType, int start, int end, CancellationToken ct) { PmcRangeReads.Add((letter, slotType, start, end)); if (ThrowOnPmcRange) throw new InvalidOperationException("transport down"); if (PmcRangeRejectors.Any(r => r(letter, start, end))) return Task.FromResult<(IReadOnlyList?, uint)>((null, PmcRangeRejectStatus)); var width = slotType switch { FocasDataType.Int16 => 2, FocasDataType.Int32 or FocasDataType.Float32 => 4, FocasDataType.Float64 => 8, _ => 1, }; var slots = new List(); for (var o = start; o + width - 1 <= end; o += width) { long v = 0; for (var k = 0; k < width; k++) v = (v << 8) | (PmcMemory.TryGetValue((letter, o + k), out var b) ? b : (byte)0); // Sign-extend like the wire parser (Int16/Int32/Int64 big-endian); width 1 stays 0..255. v = width switch { 2 => (short)v, 4 => (int)v, _ => v }; slots.Add(v); } return Task.FromResult<(IReadOnlyList?, uint)>((slots, FocasStatusMapper.Good)); } /// Writes a value to a FOCAS address asynchronously. /// The FOCAS address to write to. /// The data type of the value. /// The value to write. /// The cancellation token. public virtual Task WriteAsync( FocasAddress address, FocasDataType type, object? value, CancellationToken ct) { if (ThrowOnWrite) throw Exception ?? new InvalidOperationException(); WriteLog.Add((address, type, value)); Values[address.Canonical] = value; var status = WriteStatuses.TryGetValue(address.Canonical, out var s) ? s : FocasStatusMapper.Good; return Task.FromResult(status); } /// Probes the FOCAS connection asynchronously. /// The cancellation token. public virtual Task ProbeAsync(CancellationToken ct) => Task.FromResult(ProbeResult); /// Gets the list of active alarms. public List Alarms { get; } = []; /// Gets or sets a value indicating whether the alarm read throws (Gitea #647: a failed read). public bool ThrowOnReadAlarms { get; set; } /// Gets or sets an exception the alarm read throws (e.g. a CNC refusal carrying an rc). public Exception? AlarmReadException { get; set; } /// Gets the number of alarm reads served (successful or not). public int AlarmReadCount => Volatile.Read(ref _alarmReadCount); private int _alarmReadCount; /// Replaces the active alarm list atomically with respect to the poll thread. /// The new active alarms. public void SetAlarms(params FocasActiveAlarm[] alarms) { lock (Alarms) { Alarms.Clear(); Alarms.AddRange(alarms); } } /// Reads active alarms asynchronously. /// The cancellation token. public virtual Task> ReadAlarmsAsync(CancellationToken ct) { Interlocked.Increment(ref _alarmReadCount); if (AlarmReadException is { } refusal) throw refusal; if (ThrowOnReadAlarms) throw new InvalidOperationException("cnc_rdalmmsg2 failed (fake)"); lock (Alarms) return Task.FromResult>([.. Alarms]); } // ---- Fixed-tree T1 ---- /// Gets or sets the system information. public FocasSysInfo SysInfo { get; set; } = new(0, 3, "M", "M", "30i", "A1.0", 3); /// Gets the list of axis names. public List AxisNames { get; } = [new("X", ""), new("Y", ""), new("Z", "")]; /// Gets the list of spindle names. public List SpindleNames { get; } = [new("S", "1", "", "")]; /// Gets the dictionary of dynamic snapshots keyed by axis index. public Dictionary DynamicByAxis { get; } = []; /// Gets system information asynchronously. /// The cancellation token. public virtual Task GetSysInfoAsync(CancellationToken ct) => Task.FromResult(SysInfo); /// Gets axis names asynchronously. /// The cancellation token. public virtual Task> GetAxisNamesAsync(CancellationToken ct) => Task.FromResult>([.. AxisNames]); /// Gets spindle names asynchronously. /// The cancellation token. public virtual Task> GetSpindleNamesAsync(CancellationToken ct) => Task.FromResult>([.. SpindleNames]); /// Reads dynamic data for an axis asynchronously. /// The zero-based axis index. /// The cancellation token. public virtual Task ReadDynamicAsync(int axisIndex, CancellationToken ct) { if (!DynamicByAxis.TryGetValue(axisIndex, out var snap)) snap = new FocasDynamicSnapshot(axisIndex, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); return Task.FromResult(snap); } /// Gets or sets the program information. public FocasProgramInfo ProgramInfo { get; set; } = new("O0001", 1, 0, 1); /// Gets program information asynchronously. /// The cancellation token. public virtual Task GetProgramInfoAsync(CancellationToken ct) => Task.FromResult(ProgramInfo); /// Gets or sets the cnc_statinfo status snapshot. public FocasStatus Status { get; set; } = new(1, 3, 1, 0, 0, 0, 0, 0); /// Gets or sets a value indicating whether throws (older series / refused call). public bool ThrowOnStatus { get; set; } /// Gets the number of status reads served. public int StatusReads { get; private set; } /// Gets the CNC status flags asynchronously. /// The cancellation token. public virtual Task GetStatusAsync(CancellationToken ct) { StatusReads++; if (ThrowOnStatus) throw new InvalidOperationException("cnc_statinfo refused"); return Task.FromResult(Status); } /// Gets the dictionary of timers keyed by timer kind. public Dictionary Timers { get; } = []; /// Gets timer data asynchronously. /// The timer kind to retrieve. /// The cancellation token. public virtual Task GetTimerAsync(FocasTimerKind kind, CancellationToken ct) { if (!Timers.TryGetValue(kind, out var t)) t = new FocasTimer(kind, 0, 0); return Task.FromResult(t); } /// Gets the list of servo loads. public List ServoLoads { get; } = []; /// Gets servo loads asynchronously. /// The cancellation token. public virtual Task> GetServoLoadsAsync(CancellationToken ct) => Task.FromResult>([.. ServoLoads]); /// Gets the list of spindle load percentages (null = undescribable slot). public List SpindleLoads { get; } = []; /// Gets the list of spindle motor-speed percentages (null = undescribable slot). public List SpindleMotorSpeeds { get; } = []; /// Gets spindle loads asynchronously. /// The cancellation token. public virtual Task> GetSpindleLoadsAsync(CancellationToken ct) => Task.FromResult>([.. SpindleLoads]); /// Gets spindle motor-speed percentages asynchronously. /// The cancellation token. public virtual Task> GetSpindleMotorSpeedsAsync(CancellationToken ct) => Task.FromResult>([.. SpindleMotorSpeeds]); /// Disposes the client. public virtual void Dispose() { DisposeCount++; IsConnected = false; } } /// A factory for creating fake FOCAS clients. internal sealed class FakeFocasClientFactory : IFocasClientFactory { /// Gets the list of created clients. public List Clients { get; } = new(); /// Gets or sets a customization function for creating clients. public Func? Customise { get; set; } /// No-op usability probe — the fake factory is always usable. public void EnsureUsable() { } /// Creates a fake FOCAS client. public IFocasClient Create() { var c = Customise?.Invoke() ?? new FakeFocasClient(); Clients.Add(c); return c; } }