using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests; /// /// Bit-within-word write (B3:0/5) against ab_server on both transports: only the addressed /// bit may change, its neighbours must survive. Writes B3:0 (simulator-only, gated by /// ). PLC-5 on the native transport refuses writes (#571), which the test pins instead. /// [Collection(AbLegacyServerCollection.Name)] [Trait("Category", "Integration")] [Trait("Simulator", "ab_server-PCCC")] public sealed class AbLegacyBitWriteTests(AbLegacyServerFixture sim) { /// Seeds B3:0 = 0x0A0A, sets bit 5, clears bit 3, and checks neighbours are untouched. /// The transport stack the driver runs on. [AbLegacyTheory] [MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))] public async Task Bit_write_changes_only_the_addressed_bit(AbLegacyServerProfile profile, AbLegacyTransport transport) { if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip); IntegrationWriteGate.SkipUnlessWritesAllowed(); var deviceUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}"; await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)], RawTags = AbLegacyRawTags.Entries( new AbLegacyTagDefinition("Word", deviceUri, "B3:0", AbLegacyDataType.Int, Writable: true), new AbLegacyTagDefinition("Bit5", deviceUri, "B3:0/5", AbLegacyDataType.Bit, Writable: true), new AbLegacyTagDefinition("Bit3", deviceUri, "B3:0/3", AbLegacyDataType.Bit, Writable: true)), Timeout = TimeSpan.FromSeconds(5), Probe = new AbLegacyProbeOptions { Enabled = false }, Transport = transport, }, driverInstanceId: $"ablegacy-bitwrite-{profile.Family}-{transport}"); var ct = TestContext.Current.CancellationToken; await drv.InitializeAsync("{}", ct); var refusedNatively = profile.Family == AbLegacyPlcFamily.Plc5 && transport == AbLegacyTransport.Native; // 0x0A0A: bits 1,3,9,11 set; bit 5 clear. if (!refusedNatively) (await drv.WriteAsync([new WriteRequest("Word", (short)0x0A0A)], ct)).Single() .StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "seeding the whole word must succeed"); if (refusedNatively) { // Native PLC-5 writes are refused before the wire (#571): BadNotSupported (0x803D0000), not an ab_server gap. (await drv.WriteAsync([new WriteRequest("Bit5", true)], ct)).Single() .StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported); return; } (await drv.WriteAsync([new WriteRequest("Bit5", true)], ct)).Single() .StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "B3:0/5 bit write must succeed"); Convert.ToInt32((await drv.ReadAsync(["Word"], ct)).Single().Value).ShouldBe(0x0A0A | 0x20); (await drv.WriteAsync([new WriteRequest("Bit3", false)], ct)).Single() .StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "clearing B3:0/3 must succeed"); Convert.ToInt32((await drv.ReadAsync(["Word"], ct)).Single().Value).ShouldBe((0x0A0A | 0x20) & ~0x08); } }