feat(twincat): BOOL-within-word writes via driver-level parent-word RMW
This commit is contained in:
@@ -28,6 +28,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
private readonly ConcurrentDictionary<string, TwinCATTagDefinition> _tagsByName =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Per-parent-word RMW gate for BOOL-within-word writes: a single-bit write is a
|
||||
// read-modify-write of the parent word (TwinCAT's symbol table doesn't expose "Word.N"),
|
||||
// so concurrent bit-writers to the same word must serialise or they lose each other's
|
||||
// updates. Keyed by device + parent symbol. (Cannot guard against the PLC program writing
|
||||
// the word between our read and write — inherent to RMW.)
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _bitRmwLocks =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Resolves a read/write/subscribe fullReference to a tag definition, bridging the two
|
||||
// authoring models: an authored tag-table entry (by name) OR an equipment tag whose
|
||||
// reference is its raw TagConfig JSON (parsed once via TwinCATEquipmentTagParser, cached).
|
||||
@@ -293,6 +301,40 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
{
|
||||
var client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
var parsed = TwinCATSymbolPath.TryParse(def.SymbolPath);
|
||||
|
||||
// BOOL-within-word write — read-modify-write of the parent word. Mirrors the bit-read
|
||||
// (AdsTwinCATClient.ReadValueAsync) which reads the parent as uint: read the parent as
|
||||
// UDInt (-> uint), flip the bit, write it back, all under a per-parent lock.
|
||||
if (def.DataType == TwinCATDataType.Bool && parsed?.BitIndex is int bit)
|
||||
{
|
||||
var parentPath = (parsed with { BitIndex = null }).ToAdsSymbolName();
|
||||
var gate = _bitRmwLocks.GetOrAdd(
|
||||
$"{def.DeviceHostAddress}|{parentPath}", static _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var (parentValue, readStatus) = await client.ReadValueAsync(
|
||||
parentPath, TwinCATDataType.UDInt, null, null, cancellationToken).ConfigureAwait(false);
|
||||
if (readStatus != TwinCATStatusMapper.Good)
|
||||
{
|
||||
results[i] = new WriteResult(readStatus);
|
||||
}
|
||||
else
|
||||
{
|
||||
var word = Convert.ToUInt32(parentValue ?? 0u);
|
||||
var updated = Convert.ToBoolean(w.Value) ? word | (1u << bit) : word & ~(1u << bit);
|
||||
var writeStatus = await client.WriteValueAsync(
|
||||
parentPath, TwinCATDataType.UDInt, null, updated, cancellationToken).ConfigureAwait(false);
|
||||
results[i] = new WriteResult(writeStatus);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var symbolName = parsed?.ToAdsSymbolName() ?? def.SymbolPath;
|
||||
var status = await client.WriteValueAsync(
|
||||
symbolName, def.DataType, parsed?.BitIndex, w.Value, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user