feat(focas): cnc_statinfo status flags as Status/ fixed-tree nodes (#257)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+89
@@ -0,0 +1,89 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.Series;
|
||||
|
||||
/// <summary>
|
||||
/// Live-wire coverage (focas-mock) for #257 (<c>cnc_statinfo</c> status flags) and #266 (PMC
|
||||
/// range coalescing). Skips cleanly when <c>OTOPCUA_FOCAS_SIM_ENDPOINT</c> (default
|
||||
/// <c>localhost:8193</c>) is unreachable. Read-only: no write path is exercised.
|
||||
/// </summary>
|
||||
[Collection(FocasSimCollection.Name)]
|
||||
public sealed class WireBackendStatusAndPmcTests
|
||||
{
|
||||
private readonly FocasSimFixture _fx;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="WireBackendStatusAndPmcTests"/> class.</summary>
|
||||
/// <param name="fx">The FOCAS simulator fixture.</param>
|
||||
public WireBackendStatusAndPmcTests(FocasSimFixture fx) => _fx = fx;
|
||||
|
||||
private string DeviceHost => _fx.DeviceUri;
|
||||
|
||||
/// <summary>The mock's <c>cnc_statinfo</c> flags surface through the Status/ subtree and an authored FIXED: tag.</summary>
|
||||
[Fact]
|
||||
public async Task Status_flags_populate_via_wire_backend()
|
||||
{
|
||||
if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
await _fx.LoadProfileAsync("FWLIB64", ct);
|
||||
await _fx.PatchStateAsync(new
|
||||
{
|
||||
sysinfo = new
|
||||
{
|
||||
addinfo = 0, max_axis = 8, cnc_type = "M ", mt_type = "M ",
|
||||
series = "30i ", version = "A1.0", axes = "1 ",
|
||||
},
|
||||
axis_names = new[] { "X" },
|
||||
statinfo = new { aut = 1, run = 3, motion = 1, mstb = 0, emergency = 0, alarm = 1, edit = 0 },
|
||||
}, ct);
|
||||
|
||||
var driver = new FocasDriver(new FocasDriverOptions
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(DeviceHost)],
|
||||
RawTags = FocasRawTags.Entries(
|
||||
new FocasTagDefinition("RunFlag", DeviceHost, "FIXED:Status/Run", FocasDataType.Int32, Writable: false)),
|
||||
Probe = new FocasProbeOptions { Enabled = false },
|
||||
FixedTree = new FocasFixedTreeOptions
|
||||
{
|
||||
Enabled = true,
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
ProgramPollInterval = TimeSpan.FromMilliseconds(200),
|
||||
},
|
||||
}, driverInstanceId: "focas-wire-status", clientFactory: new WireFocasClientFactory());
|
||||
|
||||
await using (driver)
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
await WaitFor(() => driver.GetDeviceState(DeviceHost) is { LastStatus: not null }, TimeSpan.FromSeconds(5));
|
||||
|
||||
var state = driver.GetDeviceState(DeviceHost);
|
||||
state.ShouldNotBeNull();
|
||||
state.LastStatus.ShouldNotBeNull();
|
||||
state.LastStatus.Run.ShouldBe(3);
|
||||
state.LastStatus.Alarm.ShouldBe(1);
|
||||
|
||||
var snaps = await driver.ReadAsync(["RunFlag", $"{DeviceHost}/Status/Alarm", $"{DeviceHost}/Status/RunText"], ct);
|
||||
snaps.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
|
||||
snaps[0].Value.ShouldBe(3);
|
||||
snaps[1].Value.ShouldBe(1);
|
||||
snaps[2].Value.ShouldBe("START");
|
||||
|
||||
// The flags follow the machine.
|
||||
await _fx.PatchStateAsync(new { statinfo = new { run = 1, emergency = 1 } }, ct);
|
||||
await WaitFor(() => driver.GetDeviceState(DeviceHost) is { LastStatus.Run: 1 }, TimeSpan.FromSeconds(5));
|
||||
driver.GetDeviceState(DeviceHost)!.LastStatus!.Emergency.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitFor(Func<bool> pred, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (pred()) return;
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user