feat(ablegacy): B/I/O-file bit-within-word writes via existing RMW path
This commit is contained in:
@@ -360,12 +360,14 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
{
|
{
|
||||||
var parsed = AbLegacyAddress.TryParse(def.Address);
|
var parsed = AbLegacyAddress.TryParse(def.Address);
|
||||||
|
|
||||||
// PCCC bit-within-word writes — task #181 pass 2. RMW against a parallel
|
// PCCC bit-within-word writes — RMW against a parallel parent-word runtime (strip the /N
|
||||||
// parent-word runtime (strip the /N bit suffix). Per-parent-word lock serialises
|
// bit suffix). The per-parent-word lock serialises concurrent bit writers. Applies to every
|
||||||
// concurrent bit writers. Applies to N-file bit-in-word (N7:0/3) + B-file bits
|
// bit-addressable PCCC file: N-file (N7:0/3), B-file (B3:0/0), and the I/O image files
|
||||||
// (B3:0/0). T/C/R sub-elements don't hit this path because they're not Bit typed.
|
// (I:0/0, O:1/2); L-file bits RMW a 32-bit parent, the rest a 16-bit word. T/C/R sub-elements
|
||||||
if (def.DataType == AbLegacyDataType.Bit && parsed?.BitIndex is int bit
|
// don't reach this path because they're not Bit typed. NOTE: an Input-image (I) write is sent
|
||||||
&& parsed.FileLetter is not "B" and not "I" and not "O")
|
// to the PLC like any other write — the device drives the input image from physical inputs and
|
||||||
|
// may reject it; we surface that real PCCC status rather than pre-rejecting at the driver.
|
||||||
|
if (def.DataType == AbLegacyDataType.Bit && parsed?.BitIndex is int bit)
|
||||||
{
|
{
|
||||||
results[i] = new WriteResult(
|
results[i] = new WriteResult(
|
||||||
await WriteBitInWordAsync(device, parsed, bit, w.Value, cancellationToken).ConfigureAwait(false));
|
await WriteBitInWordAsync(device, parsed, bit, w.Value, cancellationToken).ConfigureAwait(false));
|
||||||
|
|||||||
@@ -105,4 +105,76 @@ public sealed class AbLegacyBitRmwTests
|
|||||||
factory.Tags["N7:0"].WriteCount.ShouldBe(2);
|
factory.Tags["N7:0"].WriteCount.ShouldBe(2);
|
||||||
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0x21); // bits 0 + 5
|
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0x21); // bits 0 + 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>B/I/O-file bit set reads the parent word, ORs the bit, writes it back (was BadNotSupported).</summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("B3:0/3", "B3:0")]
|
||||||
|
[InlineData("I:0/3", "I:0")]
|
||||||
|
[InlineData("O:1/3", "O:1")]
|
||||||
|
public async Task Bit_set_on_B_I_O_file_RMWs_parent_word(string bitAddr, string parentName)
|
||||||
|
{
|
||||||
|
var factory = new FakeAbLegacyTagFactory
|
||||||
|
{
|
||||||
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0b0001 },
|
||||||
|
};
|
||||||
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
||||||
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)],
|
||||||
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1", factory);
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
var results = await drv.WriteAsync([new WriteRequest("F", true)], CancellationToken.None);
|
||||||
|
|
||||||
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||||
|
factory.Tags.ShouldContainKey(parentName);
|
||||||
|
Convert.ToInt32(factory.Tags[parentName].Value).ShouldBe(0b1001);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>B-file bit clear preserves the other bits in the parent word.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Bit_clear_preserves_other_bits_in_B_file_word()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbLegacyTagFactory
|
||||||
|
{
|
||||||
|
Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0xFFFF) },
|
||||||
|
};
|
||||||
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
||||||
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)],
|
||||||
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1", factory);
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);
|
||||||
|
|
||||||
|
Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(unchecked((short)0xFFF7));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Concurrent bit writes to the same B-file word compose correctly (per-parent lock).</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Concurrent_bit_writes_to_same_B_file_word_compose_correctly()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbLegacyTagFactory
|
||||||
|
{
|
||||||
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
|
||||||
|
};
|
||||||
|
var tags = Enumerable.Range(0, 8)
|
||||||
|
.Select(b => new AbLegacyTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"B3:0/{b}", AbLegacyDataType.Bit))
|
||||||
|
.ToArray();
|
||||||
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
||||||
|
Tags = tags,
|
||||||
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1", factory);
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
|
||||||
|
drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));
|
||||||
|
|
||||||
|
Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user