Auto: twincat-1.3 — bit-indexed BOOL writes (RMW)
Replace the NotSupportedException at AdsTwinCATClient.WriteValueAsync for bit-indexed BOOL writes with a read-modify-write path: 1. Strip the trailing .N selector from the symbol path. 2. Read the parent as UDINT. 3. Set or clear bit N via the standard mask. 4. Write the parent back. Concurrent bit writers against the same parent serialise through a per-parent SemaphoreSlim cached in a ConcurrentDictionary (never removed — bounded by writable-bit-tag cardinality). Mirrors the AbCip / Modbus / FOCAS bit-RMW pattern shipped in #181 pass 1. The path-stripping (TryGetParentSymbolPath) and mask helper (ApplyBit) are exposed as internal statics so tests can pin the pure logic without needing a real ADS target. The FakeTwinCATClient mirrors the same RMW semantics so driver-level round-trip tests assert the parent-word state. Closes #307
This commit is contained in:
@@ -41,7 +41,25 @@ internal class FakeTwinCATClient : ITwinCATClient
|
||||
{
|
||||
if (ThrowOnWrite) throw Exception ?? new InvalidOperationException();
|
||||
WriteLog.Add((symbolPath, type, bitIndex, value));
|
||||
Values[symbolPath] = value;
|
||||
|
||||
// Model the parent-word RMW path the production AdsTwinCATClient performs for
|
||||
// bit-indexed BOOL writes so driver-level tests can assert the resulting parent state.
|
||||
if (bitIndex is int bit && type == TwinCATDataType.Bool)
|
||||
{
|
||||
var parentPath = AdsTwinCATClient.TryGetParentSymbolPath(symbolPath);
|
||||
if (parentPath is not null)
|
||||
{
|
||||
var current = Values.TryGetValue(parentPath, out var p) && p is not null
|
||||
? Convert.ToUInt32(p) : 0u;
|
||||
Values[parentPath] = AdsTwinCATClient.ApplyBit(
|
||||
current, bit, Convert.ToBoolean(value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Values[symbolPath] = value;
|
||||
}
|
||||
|
||||
var status = WriteStatuses.TryGetValue(symbolPath, out var s) ? s : TwinCATStatusMapper.Good;
|
||||
return Task.FromResult(status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATBitWriteTests
|
||||
{
|
||||
// ---- Helper unit tests ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("Flags.3", "Flags")]
|
||||
[InlineData("MAIN.Status.7", "MAIN.Status")]
|
||||
[InlineData("GVL.Motors[0].Status.5", "GVL.Motors[0].Status")]
|
||||
public void TryGetParentSymbolPath_strips_trailing_bit_selector(string input, string expected)
|
||||
{
|
||||
AdsTwinCATClient.TryGetParentSymbolPath(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Counter")] // single segment — no parent
|
||||
[InlineData(".Bad")] // leading dot
|
||||
public void TryGetParentSymbolPath_returns_null_for_pathless(string input)
|
||||
{
|
||||
AdsTwinCATClient.TryGetParentSymbolPath(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u, 0, true, 0x0000_0001u)]
|
||||
[InlineData(0u, 31, true, 0x8000_0000u)]
|
||||
[InlineData(0xFFFF_FFFFu, 0, false, 0xFFFF_FFFEu)]
|
||||
[InlineData(0xFFFF_FFFFu, 31, false, 0x7FFF_FFFFu)]
|
||||
[InlineData(0x0000_0008u, 3, true, 0x0000_0008u)] // already set — idempotent
|
||||
[InlineData(0x0000_0000u, 3, false, 0x0000_0000u)] // already clear — idempotent
|
||||
public void ApplyBit_sets_or_clears_bit(uint word, int bit, bool setBit, uint expected)
|
||||
{
|
||||
AdsTwinCATClient.ApplyBit(word, bit, setBit).ShouldBe(expected);
|
||||
}
|
||||
|
||||
// ---- Driver-level round-trip ----
|
||||
|
||||
private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewDriver(params TwinCATTagDefinition[] tags)
|
||||
{
|
||||
var factory = new FakeTwinCATClientFactory();
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = tags,
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
return (drv, factory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bit_write_sets_bit_in_parent_word()
|
||||
{
|
||||
var (drv, factory) = NewDriver(
|
||||
new TwinCATTagDefinition("FlagBit3", "ads://5.23.91.23.1.1:851", "Flags.3", TwinCATDataType.Bool));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
// Parent word starts at 0.
|
||||
factory.Customise = () => new FakeTwinCATClient { Values = { ["Flags"] = 0u } };
|
||||
|
||||
var results = await drv.WriteAsync(
|
||||
[new Core.Abstractions.WriteRequest("FlagBit3", true)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
// Parent should now have bit 3 set.
|
||||
Convert.ToUInt32(factory.Clients[0].Values["Flags"]!).ShouldBe(0x0000_0008u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bit_write_clears_bit_without_disturbing_neighbours()
|
||||
{
|
||||
var (drv, factory) = NewDriver(
|
||||
new TwinCATTagDefinition("FlagBit3", "ads://5.23.91.23.1.1:851", "Flags.3", TwinCATDataType.Bool));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
// Parent word has bits 0, 3, 7 set initially.
|
||||
factory.Customise = () => new FakeTwinCATClient { Values = { ["Flags"] = 0x0000_0089u } };
|
||||
|
||||
var results = await drv.WriteAsync(
|
||||
[new Core.Abstractions.WriteRequest("FlagBit3", false)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
// Bit 3 cleared; bits 0 and 7 untouched.
|
||||
Convert.ToUInt32(factory.Clients[0].Values["Flags"]!).ShouldBe(0x0000_0081u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bit_write_does_not_throw_NotSupported()
|
||||
{
|
||||
// Regression: AdsTwinCATClient previously threw NotSupportedException for bit writes.
|
||||
var (drv, factory) = NewDriver(
|
||||
new TwinCATTagDefinition("Bit", "ads://5.23.91.23.1.1:851", "GVL.Word.0", TwinCATDataType.Bool));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.Word"] = 0u } };
|
||||
|
||||
var results = await drv.WriteAsync(
|
||||
[new Core.Abstractions.WriteRequest("Bit", true)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
// Status should be Good, not BadNotSupported (which is what the catch block produced).
|
||||
results.Single().StatusCode.ShouldNotBe(TwinCATStatusMapper.BadNotSupported);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user