diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs index 6864a4e1..81fe2897 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs @@ -1232,13 +1232,16 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, ProgramInfo: programInfo is not null, Timers: timer is not null, Status: statusProbe is not null); - state.LastStatus = statusProbe; - state.FixedTreeCache = new FocasFixedTreeCache(sys, [.. axes], [.. spindles], caps); // The bootstrap IS an answer from the device, and Identity comes entirely from it — // so stamp liveness here as well as per tick, or a CNC whose axis poll never // succeeds would serve a perfectly valid sysinfo as stale. state.LastFixedPollUtc = DateTime.UtcNow; + // Published LAST (release write): LastStatus is the field callers watch for "the first + // answer is in", and an authored FIXED:Status/* read is only servable once + // FixedTreeCache and LastFixedPollUtc are set (TryReadFixedTreeOnDevice). Publishing it + // first let a reader see a status while the read still returned BadCommunicationError (#673). + state.LastStatus = statusProbe; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; } catch (Exception ex) @@ -1998,7 +2001,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// public ConcurrentDictionary CoalesceRejected { get; } = new(StringComparer.Ordinal); /// Gets or sets the last cnc_statinfo snapshot (#257); null until the first answer. - public FocasStatus? LastStatus { get; set; } + public FocasStatus? LastStatus + { + get => Volatile.Read(ref _lastStatus); + set => Volatile.Write(ref _lastStatus, value); + } + private FocasStatus? _lastStatus; /// Gets or sets the last program information snapshot. public FocasProgramInfo? LastProgramInfo { get; set; } /// Gets or sets the cached first-axis dynamic snapshot — feeds Program/Number, /MainNumber, /Sequence. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasStatusFlagsTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasStatusFlagsTests.cs index 8ee12585..42bdf191 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasStatusFlagsTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasStatusFlagsTests.cs @@ -58,7 +58,7 @@ public sealed class FocasStatusFlagsTests var client = new FakeFocasClient { Status = new FocasStatus(Auto: 1, Run: 3, Motion: 1, Mstb: 2, Emergency: 0, Alarm: 1, Edit: 0, TmMode: 4) }; var drv = NewDriver(client); await drv.InitializeAsync("{}", CancellationToken.None); - await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null); + await WaitAsync(() => drv.GetDeviceState(Host) is { LastFixedPollUtc: not null, FixedTreeCache: not null, LastStatus: not null }); string[] fields = ["Auto", "Run", "Motion", "Mstb", "Emergency", "Alarm", "Edit", "TmMode"]; int[] expected = [1, 3, 1, 2, 0, 1, 0, 4]; @@ -82,7 +82,8 @@ public sealed class FocasStatusFlagsTests var drv = NewDriver(client, new FocasTagDefinition("EStop", Host, "FIXED:Status/Emergency", FocasDataType.Int32, Writable: false)); await drv.InitializeAsync("{}", CancellationToken.None); - await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null); + // Wait for full readiness: an authored FIXED read needs the cache and the poll stamp too (#673). + await WaitAsync(() => drv.GetDeviceState(Host) is { LastFixedPollUtc: not null, FixedTreeCache: not null, LastStatus: not null }); var snap = (await drv.ReadAsync(["EStop"], CancellationToken.None))[0]; snap.StatusCode.ShouldBe(FocasStatusMapper.Good); @@ -97,7 +98,7 @@ public sealed class FocasStatusFlagsTests var client = new FakeFocasClient { Status = new FocasStatus(1, 0, 0, 0, 0, 0, 0, 0) }; var drv = NewDriver(client); await drv.InitializeAsync("{}", CancellationToken.None); - await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null); + await WaitAsync(() => drv.GetDeviceState(Host) is { LastFixedPollUtc: not null, FixedTreeCache: not null, LastStatus: not null }); client.Status = new FocasStatus(1, 3, 1, 0, 0, 0, 0, 0); await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus?.Run == 3);