Files
RMW pass 2 — AbCip BOOL-within-DINT + AbLegacy bit-within-word. Closes task #181. AbCip — AbCipDriver.WriteAsync now detects BOOL writes with a bit index + routes them through WriteBitInDIntAsync: strip the .N suffix to form the parent DINT tag path (via AbCipTagPath with BitIndex=null + ToLibplctagName), get/create a cached parent IAbCipTagRuntime via EnsureParentRuntimeAsync (distinct from the bit-selector tag runtime so read + write target the DINT directly), acquire a per-parent-name SemaphoreSlim, Read → Convert.ToInt32 the current DINT → (current | 1<<bit) or (current & ~(1<<bit)) → Write via EncodeValue(DInt, updated). Per-parent lock prevents concurrent writers to the same DINT from losing updates — parallels Modbus + FOCAS pass 1. DeviceState gains ParentRuntimes dict + GetRmwLock helper + _rmwLocks ConcurrentDictionary. DisposeHandles now walks ParentRuntimes too. LibplctagTagRuntime.EncodeValue's BOOL-with-bitIndex branch stays as a defensive throw (message updated to point at the new driver-level dispatch) so an accidental bypass fails loudly rather than silently clobbering the whole DINT. AbLegacy — identical pattern for PCCC N-file bit writes. AbLegacyDriver.WriteAsync detects Bit with bitIndex + PMC letter not in {B, I, O} (B-file + I/O use their own bit-addressable semantics so don't RMW at N-file word level), routes through WriteBitInWordAsync which uses Int16 for the parent word, creates + caches a parent runtime with the suffix-stripped N7:0 address, acquires per-parent lock, RMW. DeviceState extended the same way as AbCip (ParentRuntimes + GetRmwLock). LibplctagLegacyTagRuntime.EncodeValue Bit-with-bitIndex branch points at the driver dispatch. Tests — 5 new AbCipBoolInDIntRmwTests (bit set ORs + preserves, bit clear ANDs + preserves, 8-way concurrent writes to same parent compose to 0xFF, different-parent writes get separate runtimes, repeat bit writes reuse the parent runtime init-count 1 + write-count 2), 4 new AbLegacyBitRmwTests (bit set preserves, bit clear preserves 0xFFF7, 8-way concurrent 0xFF, repeat writes reuse parent). Two pre-existing tests flipped — AbCipDriverWriteTests.Bit_in_dint_write_returns_BadNotSupported + AbLegacyReadWriteTests.Bit_within_word_write_rejected_as_BadNotSupported both now assert Good instead of BadNotSupported, renamed to _now_succeeds_via_RMW. Total tests — AbCip 166/166, AbLegacy 96/96, full solution builds 0 errors; Modbus + FOCAS + TwinCAT + other drivers untouched. Task #181 done across all four libplctag-backed + non-libplctag drivers (Modbus BitInRegister + AbCip BOOL-in-DINT + AbLegacy N-file bit + FOCAS PMC Bit — all with per-parent-word serialisation).
AB Legacy PR 3 — ITagDiscovery + ISubscribable + IHostConnectivityProbe + IPerCallHostResolver. Fills out the AbLegacy capability surface — the driver now implements the same 7-interface set as AbCip (IDriver + IReadable + IWritable + ITagDiscovery + ISubscribable + IHostConnectivityProbe + IPerCallHostResolver). ITagDiscovery emits pre-declared tags under an AbLegacy root folder with a per-device sub-folder keyed on HostAddress (DeviceName fallback to HostAddress when null). Writable tags surface as SecurityClassification.Operate, non-writable as ViewOnly. No controller-side enumeration — PCCC has no @tags equivalent on SLC / MicroLogix / PLC-5 (symbol table isn't exposed the way Logix exposes it), so the pre-declared path is the only discovery mechanism. ISubscribable consumes the shared PollGroupEngine extracted in AB CIP PR 1 — reader delegate points at ReadAsync (already handles lazy runtime init + caching), onChange bridges into the driver's OnDataChange event. 100ms interval floor. Initial-data push on first poll. Makes AbLegacy the third consumer of PollGroupEngine (after Modbus and AbCip). IHostConnectivityProbe — per-device probe loop when ProbeOptions.Enabled + ProbeAddress configured (defaults to S:0 status file word 0). Lazy-init on first tick, re-init on wire failure (destroyed native handle gets recreated rather than silently staying broken). Success transitions device to Running, exception to Stopped, same-state spurious event guard under per-device lock. GetHostStatuses returns one entry per device with current state + last-change timestamp for Admin /hosts surfacing. IPerCallHostResolver maps tag full-ref → DeviceHostAddress for the Phase 6.1 (DriverInstanceId, ResolvedHostName) bulkhead/breaker keying per plan decision #144. Unknown refs fall back to first device's address (invoker handles at capability level as BadNodeIdUnknown); no devices → DriverInstanceId. ShutdownAsync cancels + disposes each probe CTS, disposes PollGroupEngine cancelling active subscriptions, disposes every cached runtime. DeviceState gains ProbeLock / HostState / HostStateChangedUtc / ProbeCts / ProbeInitialized matching AbCip's DeviceState shape. 10 new unit tests in AbLegacyCapabilityTests covering — pre-declared tags emit under AbLegacy/device folder with correct SecurityClassification, subscription initial poll raises OnDataChange with correct value, unsubscribe halts polling (value change post-unsub produces no further events), GetHostStatuses returns one entry per device, probe Running transition on successful read, probe Stopped transition on read exception, probe disabled when ProbeAddress null, ResolveHost returns declared device for known tag, falls back to first device for unknown, falls back to DriverInstanceId when no devices. Total AbLegacy unit tests now 92/92 passing (+10 from PR 2's 82); full solution builds 0 errors; AbCip + Modbus + other drivers untouched. AB Legacy driver now complete end-to-end — SLC 500 / MicroLogix / PLC-5 / LogixPccc all shippable with read / write / discovery / subscribe / probe / host-resolve, feature-parity with AbCip minus IAlarmSource (same deferral per plan).