Driver.MTConnect-005/-006/-007, each TDD'd red-first.
- -005: _options was the one field at the StartCoreAsync commit point published by a
plain write and read lock-free from ReadAsync/WriteAsync/DiscoverAsync/HostName/health.
Now Volatile.Write/Volatile.Read like _session/_binding/_announcedInstanceId, pinned by
a source scan (MTConnectOptionsPublicationTests) that fails on any unfenced touch.
- -006: TryFormatValue guarded null and Array, but Convert.ToString renders ANY
non-IConvertible value as its type name under a Good status. Guard is now the property
the remark relies on; the accepted set (string/bool/numeric/DateTime) is unchanged.
- -007: MTConnectAgentClient.Dispose was a check-then-set on a non-volatile bool, so two
racing disposals both passed and the loser cancelled an already-disposed CTS. Now an
Interlocked.Exchange claim over an int, with the request seams reading it volatile.
TryFormatValue's catch filter named only FormatException / InvalidCastException
/ OverflowException, and the call sits OUTSIDE WriteAsync's per-slot try. But
Convert.ToString falls back to the value's OWN ToString/IFormattable - arbitrary
caller code that may throw anything - so any other exception escaped WriteAsync
uncaught and failed the entire batch, breaking the documented never-throws
per-slot contract. Broadened to a catch-all; the slot gets BadTypeMismatch and
the exception is logged at Warning (TryFormatValue became an instance method to
reach _logger). One unformattable value now costs its own slot and nothing more.
Falsification checked, not assumed: restoring the narrow filter makes the new
test fail with a raw "System.InvalidOperationException : this formatter is
broken" propagating out of WriteAsync - i.e. it fails for the contract-breach
reason, not on an assertion.
Two tests added:
- a value whose ToString throws InvalidOperationException is refused
BadTypeMismatch while the *following* good slot still writes (the load-bearing
half - it proves the batch survived).
- mid-batch cancellation: cancellation injected from inside slot 1's own
formatting, so slot 0 has provably already reached the Agent. It keeps its
real Good outcome while slots 1-2 fill BadCommunicationError. Reporting a
landed write as failed is worse than reporting a failure, and a fill that
started at 0 instead of i would do exactly that.
Tests: 534 passed / 0 failed (was 532). Solution build: 0 errors, no warnings
in any MTConnect project.
MTConnectDriver now implements IWritable over the Task-1 agent-client seam
(WriteObservationAsync -> the Agent's AllowPut POST surface). A write is
OBSERVATION INJECTION, not machine command: it sets what the Agent reports for
a DataItem, is visible to every consumer of that Agent, and is overwritten by
the next adapter/SHDR update. Off at two independent gates, both off by default
- the Agent's own AllowPut and the driver's write.enabled (Task 0).
Per-slot refusal order (never throws, strict N-in/N-out batch shape):
write disabled -> BadNotWritable
no live client -> BadNotConnected (ReadAsync's existing convention)
reference unresolved -> BadNodeIdUnknown
no resolvable device -> BadNodeIdUnknown
CONDITION category -> BadNotWritable
null / non-scalar value -> BadTypeMismatch
agent refusal OR throw -> BadCommunicationError
OperationCanceledException fills the remaining slots and returns. The Agent's
refusal text is logged at Warning and never enters DriverHealth.LastError
(type-only discipline); a refused write leaves health untouched.
Device-name resolution: TagBinding gains WriteTargetsByDataItemId, built in
BuildBinding from a probe-model walk that remembers which device each DataItem
was declared under (name, or id when nameless) plus its category. Falls back to
options.DeviceName; unresolvable -> BadNodeIdUnknown rather than a guess.
Values render via Convert.ToString(..., InvariantCulture) - so a Boolean writes
the invariant string "True"/"False", NOT "1"/"0". Arrays are refused rather
than rendered: Convert.ToString would emit the literal "System.Double[]", which
the Agent stores happily and which is invisible until read-back.
FINDING - the CurrentWrite bit does NOT key on SecurityClassification, and the
discovery stamping below is therefore cosmetic. Traced end to end: a browsed
leaf's SecurityClass survives into the browse session's AttributeInfo
(CapturedTreeBrowseSession.cs:53) and stops there - RawBrowseModal never reads
it, RawBrowseCommitMapper.MapLeaf hardcodes TagAccessLevel.Read, and Tag has no
SecurityClass column. AddressSpaceComposer:746/:809 sets Writable purely from
Tag.AccessLevel == ReadWrite; AddressSpaceApplier ANDs !IsArray;
OtOpcUaNodeManager.ComposeAccessLevel:2002 composes CurrentWrite from that
alone. Driver capability is never consulted at compose time either - IWritable
is checked only at runtime in DriverInstanceActor:755. Consequences: (a) raw
tags already committed keep their authored AccessLevel, so nothing this commit
does makes an existing node writable; (b) an operator must still set the tag to
ReadWrite for an MTConnect write to be reachable over OPC UA; (c) conversely a
ReadWrite tag on a non-IWritable driver has always advertised CurrentWrite and
failed only at the actor - pre-existing, not touched here.
Discovery stamps CanWrite(writeEnabled, dataItem) = write.enabled && !CONDITION
-> Operate, else ViewOnly - kept honest so the browse never advertises a tier
the write path would refuse (MQTT's CanWrite principle), with the caveat above
recorded verbatim at the call site.
Status-code constants are spelled WITHOUT MqttDriver's "Status" prefix, to
match the two this file already declares (BadCommunicationError,
BadNotConnected). StatusCodeParityTests strips either spelling and passes.
TDD: WriteAsync landed first as a NotImplementedException scaffold - 19 of 20
new tests red on behaviour, 1 (the capability assertion) green on the scaffold
- then implemented. MTConnectFactoryTests' "not_writable" characterization test
is rewritten to pin the new truth, and is the guard for the concrete-return
contract the factory remark explains.
Tests: 532 passed / 0 failed (was ~509). Full solution build: 0 errors,
0 warnings; OTOPCUA0001 clean.