An ALARM: address (*, <Type>, <Type>/<n>, <Type>/<lo>-<hi>, T<code>, OTHER) on a raw tag that
also carries the cross-driver alarm object is ONE condition with ConditionId == RawPath and a
Kind (Raise / Retrigger / Clear), so DriverHostActor.ForwardNativeAlarm routes it and
NativeAlarmProjector activates it. Fixes all seven triage defects:
1. SubscribeAlarmsAsync treated the host's RawPaths as device hosts -> nothing polled.
2. Decorated "{host}#{Type}:{Number}" ConditionId -> every event dropped.
3. No Kind -> projector kept prior state.
4. WireFocasClient.ReadAlarmsAsync answered [] on failure -> mass clear + un-acked re-raise.
It now throws; a failed device read keeps every condition's state.
5. Acks lost on reconnect -> told-record dedup (MTConnect #626 pattern) held for the driver's
lifetime; same-set re-subscribe / re-init re-raise nothing; changed set prunes leavers only.
6. Subscribe threw while AlarmProjection.Enabled=false -> the tag is the opt-in; flag is a no-op.
7. Alarm tags could not be authored -> ALARM: grammar (FocasAlarmSelector, Contracts) backs the
editor, the address picker, the deploy-time inspector and driver init.
Severity: authored alarm.severity -> exact OpcUaSeverity; else the 16i-era type bucket. The type
table stays the only one (moved to Contracts, documented 16i-era; 30i codes not invented; raw
T<code> selector + raw code in every message). Read-only: ack is server-side. Also clears the
value table on a re-initialise (#630 gap that let a removed/moved tag keep reading).
E2E: Runtime.Tests FocasNativeAlarmEndToEndTests drives the real driver through the real
DriverHostActor and asserts the condition goes Active then inactive.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The mock had served the shapes the driver once agreed with it on, so it
validated the very defects just fixed: it ANSWERED the non-existent 0x00D3
cnc_getfigure, wrote base/exponent 0/0 into position blocks, put the macro
decimal at offset 4, and served spindle 'max rpm' as a raw constant.
Ported from the sister ignitionoee mock (local divergence recorded in
VENDORED.md; the LE timer and alarm cap are kept):
- 0x00D3 refused with EW_FUNC, as the real control does.
- 0x0026 blocks carry base 10 / position_figures exponent; signed arg2,
arg2 = 0 or past the axis count refused EW_ATTRIB.
- cnc_rdmacro is the 8-byte ODBM shape, 9-digit mantissa, unset = vacant.
- servo/spindle meter records carry the envelope; spindle meters are
1/16384-of-full-scale counts (motor speed = speed / max_rpm).
Integration tests: spindle test reads Load/MotorSpeedPct percentages; the
position-scale test now proves the block's own scale wins; new
Vacant_positions_and_macros_read_bad_over_the_wire. 12/12 pass against the
mock run locally (python -m focas_mock.cli serve, OTOPCUA_FOCAS_SIM_ENDPOINT).
A CNC can report duplicate sibling names — the wonder Makino returns two
axes both called AA — which made the Axes/ subtree untrustworthy: the
browser offered two identical folders the commit gate had to refuse, and
the poll loop's name-keyed snapshot cache let the second axis silently
overwrite the first. Spindles and servo loads had the same collision in
their own keying, plus a resolve-to-first-match on the spindle read path.
Scheme: every member of a collision set gets a 1-based occurrence-ordinal
suffix in slot order (AA_1, AA_2); unique names stay bare. Chosen over the
alternatives deliberately:
- Names, not slot numbers, stay the primary identity — the segment map is
re-derived from the live device each session, so a name-keyed path
follows the physical axis across config changes, where a slot-qualified
path would silently rebind on any axis-list shift and rename every axis
on every machine for one pathological CNC.
- The WHOLE collision set is suffixed: bare AA ceases to exist when
ambiguous, so a tag authored against it reads BadNodeIdUnknown rather
than one arbitrary axis's data — the #534/#540 silent-wrongness class.
- Occurrence ordinal, not absolute slot: inserting/removing an unrelated
axis never renames an existing duplicate pair. If the collision-set
membership itself changes, segments change on the next browse and stale
authored segments read Bad, never another axis's data.
One pure helper (FixedTreeNames.Disambiguate, collision-proof against
genuine names by suffix escalation) feeds FocasFixedTreeCache.AxisSegments
/ SpindleSegments, and the emitter, axis snapshot keys, servo-load keys
(cnc_rdsvmeter answers in slot order, so the k-th AA load belongs to the
k-th AA axis) and the spindle read resolution all consume those — they can
no longer disagree. The nameless-spindle S{n} fallback applies before
disambiguation so a fallback name colliding with a real one is handled
like any other duplicate.
Tests are red-first on the pre-fix driver (all five driver-level tests
fail on master's code): browse emits AA_1/AA_2 and no bare AA, each
duplicate reads its own position and servo load, duplicate spindles
resolve per slot, bare AA reads Bad. The wire half runs live against
focas-mock (duplicate names survive the managed wire parse and publish as
suffixed segments); the mock's dynamic store is name-keyed so the
distinct-values half stays with the offline tests. FOCAS 327/327,
integration 11/11, FOCAS.Cli 52, AdminUI 1113, ControlPlane 231.
Closes#548.
Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
Replace pre-declared options.Tags (typed <Driver>TagDefinition lists) with
options.RawTags (IReadOnlyList<RawTagEntry>) across all 6 Docker-gated driver
integration suites so they COMPILE against the v3 driver API.
Each project gets a local <Driver>RawTags helper mirroring the unit-test
project's helper: it serialises a typed def back to its TagConfig blob via the
driver's <Driver>TagDefinitionFactory.ToTagConfig and packages it as a
RawTagEntry (RawPath = def.Name; DeviceName = def.DeviceHostAddress for the
multi-device drivers AbCip/AbLegacy/TwinCAT/FOCAS). Test intent (addresses,
data types, device routing, assertions) is unchanged — only the
tag-authoring/config-delivery shape was migrated.
Docker-gated: these still skip at runtime without fixtures; COMPILE is the gate.
Two test/fixture bugs; NO OtOpcUa driver change.
1. Topology: FocasSimFixture reads an env-overridable endpoint
(OTOPCUA_FOCAS_SIM_ENDPOINT, default localhost:8193) and exposes Host/Port,
but WireBackendTests + WireBackendCoverageTests hardcoded
focas://127.0.0.1:8193. Against a remote fixture the skip-gate passed
(remote reachable) but the driver dialed localhost -> KeyNotFound. Added a
DeviceUri property to the fixture; each test now derives DeviceHost from it.
Recovered 8 of 9.
2. Mock timer endianness (surfaced once #1 let the tests reach the mock): the
last failure read 60397977600 for a 3600 s power-on timer (= 0x3C000000 x 60).
The focas-mock's _wire_timer encoded the minute/msec fields big-endian (_u32),
but cnc_rdtimer's timer fields are LITTLE-endian on real hardware — a
documented, live-31i-B-validated quirk that FocasWireClient.ParseTimer decodes
LE (docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md). The driver was
correct; the mock was wrong. _wire_timer now emits little-endian (_u32_le).
Suite 9F/1P -> 10/10 against the remote fixture at ~/otopcua-focas.
Integration-sweep follow-up #3.
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.
- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
integration, install).
Build green (0 errors); unit tests pass. Docs left for a separate pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>