Every value verified against Opc.Ua.StatusCodes in the pinned SDK (1.5.378.106) by
reflection, not by copying siblings. All are client-visible: OPC UA clients branch on
status.
The three named in #497:
- Historian.Gateway SampleMapper "BadNoData" 0x800E0000 -> 0x809B0000
(0x800E0000 is BadServerHalted)
- Galaxy "BadTimeout" 0x800B0000 -> 0x800A0000
(0x800B0000 is BadServiceUnsupported)
- TwinCAT BadTypeMismatch 0x80730000 -> 0x80740000
(0x80730000 is BadWriteNotSupported)
BadTypeMismatch was wrong in three MORE drivers the issue did not name — FOCAS,
AbLegacy and AbCip carried the same 0x80730000. Every call site is a
FormatException/InvalidCastException conversion failure, so the name was right and
the value was wrong in all four.
Adding the reflection guard then surfaced nine further defects offline tests had
never checked:
- Galaxy GoodLocalOverride 0x00D80000 -> 0x00960000 (not a UA code at all)
- Galaxy UncertainLastUsableValue 0x40A40000 -> 0x40900000
- Galaxy UncertainSensorNotAccurate 0x408D0000 -> 0x40930000
- Galaxy UncertainEngineeringUnitsExceeded 0x408E0000 -> 0x40940000
- Galaxy UncertainSubNormal 0x408F0000 -> 0x40950000
- TwinCAT BadOutOfService 0x80BE0000 -> 0x808D0000 (was BadProtocolVersionUnsupported)
- TwinCAT BadInvalidState 0x80350000 -> 0x80AF0000 (was BadAttributeIdInvalid)
- AbCip/AbLegacy GoodMoreData 0x00A70000 -> 0x00A60000 (was GoodCommunicationEvent)
- Historian.Gateway GatewayQualityMapper Good_LocalOverride 0x00D80000 -> 0x00960000
Galaxy's whole Uncertain block read as though the OPC DA quality byte could be
shifted into the UA substatus position; it cannot — the substatuses are an unrelated
enumeration. GatewayQualityMapper already held the correct table, which is what the
Galaxy values are now reconciled against.
Guard: StatusCodeParityTests (Core.Abstractions.Tests, which already project-
references every driver) reflects over every `const uint` in the deployed
ZB.MOM.WW.OtOpcUa.Driver.*.dll whose name reads as a status code, and asserts it
equals Opc.Ua.StatusCodes.<name>. Discovery is by convention, so a new driver
assembly referenced by that project is covered with no edit here. It went red on all
nine defects above before the fixes and now checks 109 constants green.
Because reflection can only see a NAMED constant, two inline literals were hoisted so
the guard can reach them: Galaxy's BadTimeout/Bad into StatusCodeMap, and
GatewayQualityMapper's 15-entry table into a new HistorianStatusCodes. An inline
`0x800B0000u, // BadTimeout` at a call site is exactly how the Galaxy defect survived.
The drivers stay SDK-free by design; only the test project references Opc.Ua.Core,
as the oracle.
Also corrected: tests that pinned the wrong values (Galaxy StatusCodeMapTests,
SampleMapperTests, GatewayQualityMapperTests — all written from the same bad source),
a mislabelled comment in DriverInstanceActorTests, and stale constants in two design
docs, one of them the unexecuted MTConnect driver design that would have propagated
BadNoData's wrong value into a new driver.
The CLI's SnapshotFormatter value->name table was checked against the SDK and is
correct; no change needed.
The historian's Int1 analog-tag creation path is server-degenerate:
EnsureTags(Int1) stores an unusable stub (live-probed on the deployed
2023 R2 historian; root-caused + documented gateway-side in
HistorianGateway #11 / PR #16). HistorianTypeMapper mapped
DriverDataType.Boolean -> HistorianDataType.Int1, so Boolean historized
tags failed auto-provisioning.
Map Boolean -> Int2 instead (store 0/1). Int2 is a proven-creatable
analog type, and the value-write path already sends every value as a
double on WriteLiveValues regardless of tag type, so Boolean 0.0/1.0
round-trips cleanly. Both Float and Int2 are supported, so a
pre-existing Float Boolean tag retypes cleanly on its next deploy.
- HistorianTypeMapper: Boolean => Int2 (+ remarks)
- Unit pins: HistorianTypeMapperTests, GatewayTagProvisionerTests
(Boolean_definition_carries_explicit_Int2_presence)
- Live gate: EnsureTags_boolean_sandbox_provisions_as_Int2 (was _Int1);
retype probe reframed Float->Int2 (drops the dead Int1 leg)
- Docs: docs/Historian.md 'Boolean historization maps to Int2', CLAUDE.md,
archreview STATUS.md (R2-06 live gate -> 6/6)
Resolves#441.
Live validation against wonder-sql-vd03 caught that the gateway's SendEvent handler
throws when the wire event carries a client-supplied Id — so every alarm send from
OtOpcUa failed (PermanentFail). AlarmEventMapper now leaves HistorianEvent.Id unset
(the historian assigns event identity) and preserves the alarm's id as an 'AlarmId'
property. With this, the live alarm send acks.
Also harden the env-gated live tests against two gateway/historian-side limitations
surfaced during validation (neither an OtOpcUa defect): the write readback uses a
timezone-tolerant window (an explicit-timestamp WriteLiveValues lands offset by the
deployment's local-vs-UTC delta — reproducible via raw grpcurl; OtOpcUa sends correct
UTC), and the alarm ReadEvents readback skips with a clear reason when the historian's
server-gated event reads (C2, won't-fix) return nothing. Read + write-persist +
alarm-send are all live-validated green; the alarm send-ack is split into its own test.
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii