From 56ccaa797c88a02529d588abf4f8b5a758481343 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 12:03:54 -0400 Subject: [PATCH] docs(drivers): B/I/O + BOOL-within-word bit writes (RMW) --- docs/drivers/AbLegacy.md | 12 +++++++++++- docs/drivers/TwinCAT.md | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/drivers/AbLegacy.md b/docs/drivers/AbLegacy.md index 99d39819..1241876d 100644 --- a/docs/drivers/AbLegacy.md +++ b/docs/drivers/AbLegacy.md @@ -40,7 +40,7 @@ scope. |------------|---------------------------|-------| | `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders. Tags are single-element today (`IsArray` hard-wired false); multi-element file ranges are a tracked follow-up. | | `IReadable` | `ReadAsync` | Per-tag reads serialized per cached runtime under a lock (a libplctag `Tag` handle is not concurrency-safe across the server read path + poll loop). | -| `IWritable` | `WriteAsync` | Bit-within-word writes (N-file `N7:0/3`, B-file bits) do a per-parent-word read-modify-write under a lock. Non-writable tags return `BadNotWritable`. | +| `IWritable` | `WriteAsync` | Bit-within-word writes (N/L-file `N7:0/3`, B-file `B3:0/0`, and I/O image files `I:0/0`/`O:1/2`) do a per-parent-word read-modify-write under a lock. An Input-image (`I`) write is forwarded to the PLC and may be rejected by the device (the device drives the input image from physical inputs); the driver surfaces the device's real PCCC status in that case. Non-writable tags return `BadNotWritable`. | | `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | No push model — subscriptions become polling groups. | | `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | One probe loop per device reading `Probe.ProbeAddress`; transitions log Warning (down) / Information (recover). | | `IPerCallHostResolver` | `ResolveHost` | Routes each call to the tag's `DeviceHostAddress`; unknown references fall back to the first device, never throwing (per the interface contract). | @@ -75,6 +75,16 @@ PCCC structural rules — bit-addressing only on 16/32-bit element files, sub-elements only on T/C/R files, no file number on I/O/S — rejecting malformed addresses before they reach libplctag. +**Bit-within-word writes** — B-file (`B3:0/0`), I/O image (`I:0/0`, `O:1/2`), +N-file (`N7:0/3`), and L-file bit addresses all use the same parent-word +read-modify-write (RMW) path: the driver reads the parent word, flips the target +bit, and writes the word back, under a per-parent lock. The lock serialises the +driver's own concurrent bit-writers against the same word, but cannot guard +against the PLC program writing that word between the driver's read and write — +this is an inherent limitation of any software-level RMW. Input-image (`I`) bit +writes are forwarded to the PLC as-is; the device drives the input image from +physical inputs and may reject the write. + ## Configuration `AbLegacyDriverOptions` (`Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs`) diff --git a/docs/drivers/TwinCAT.md b/docs/drivers/TwinCAT.md index 05b0ac2c..9f7e2998 100644 --- a/docs/drivers/TwinCAT.md +++ b/docs/drivers/TwinCAT.md @@ -62,6 +62,14 @@ mirrors IEC 61131-3 structured-text identifiers: global-variable-list (`Motor1.Status.Running`), array subscripts (`Data[5]`, `Matrix[1,2]`), and bit-access (`Flags.0`). +**BOOL-within-word writes** — writing a bit-access path (e.g. `MAIN.Flags.3`) +is a driver-level parent-word read-modify-write: the driver reads the parent +word as `UDInt`, flips the target bit, and writes the word back, under a +per-parent lock. The lock serialises the driver's own concurrent bit-writers +against the same word, but cannot guard against the PLC program writing that +word between the driver's read and write — this is an inherent limitation of +any software-level RMW. + ## Tag discovery `DiscoverAsync` always emits the pre-declared `Tags` as the authoritative config @@ -79,7 +87,7 @@ discovery. | Capability | Path | Notes | |------------|------|-------| | `IReadable` | `ReadAsync` → ADS `ReadValueAsync` | Per-device client, lazily connected and serialized per device | -| `IWritable` | `WriteAsync` → ADS `WriteValueAsync` | Read-only tags return `BadNotWritable` | +| `IWritable` | `WriteAsync` → ADS `WriteValueAsync` | BOOL-within-word writes (e.g. `MAIN.Flags.3`) use a driver-level parent-word read-modify-write (read as UDInt, flip bit, write back) under a per-parent lock. Read-only tags return `BadNotWritable`. | | `ITagDiscovery` | `DiscoverAsync` | Pre-declared tags + opt-in controller symbol browse | | `ISubscribable` | native ADS notifications (default), poll fallback | `UseNativeNotifications=true` registers device notifications so the PLC pushes changes; `false` uses the shared `PollGroupEngine` | | `IHostConnectivityProbe` | per-device probe loop | One `HostConnectivityStatus` per configured device; `Running`/`Stopped` transitions raise `OnHostStatusChanged` |