cd4b29a36879110c44311a4fe38540d3fd7aadb6
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b833dda673 |
fix(mtconnect): a throwing value formatter cannot escape the write batch
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. |
||
|
|
b29623cf99 |
feat(mtconnect): IWritable via agent observation writes
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. |