Phase 3 PR 73 -- OPC UA Client browse enrichment #72

Merged
dohertj2 merged 1 commits from phase-3-pr73-opcua-client-browse-enrichment into v2 2026-04-19 02:02:40 -04:00
Owner

Summary

Two-pass discovery: pass 1 browses + collects PendingVariable tuples (folders still register inline), pass 2 batch-reads DataType + ValueRank + UserAccessLevel + Historizing per variable in one Session.ReadAsync, then registers each with accurate DriverAttributeInfo.

Replaces the prior conservative Int32 / ViewOnly / not-array placeholders with real metadata:

  • MapUpstreamDataType — Boolean, Int16/32/64, UInt16/32/64, Float32/Float64, String, DateTime/UtcTime; SByte+Byte widen to Int16; custom NodeIds fall back to String.
  • MapAccessLevelToSecurityClass — reads AccessLevels.CurrentWrite bit → Operate or ViewOnly. Uses UserAccessLevel (post-ACL-filter) not AccessLevel.
  • IsArray from ValueRank (−1 = scalar). IsHistorized from server flag — gates PR 76 history routing.

Graceful degradation: individual attribute Bad → type default; wholesale enrichment failure → register every pending variable with fallback defaults. Browse never produces an empty address space when the first pass succeeded.

Validation

  • 51/51 OpcUaClient.Tests pass (20 new mapping)
  • dotnet build: 0 errors

Test plan

  • 11-type standard DataType theory
  • SByte+Byte widened to Int16 (with TODO comment for Core.Abstractions extension)
  • UtcTime → DateTime
  • Unknown type → String fallback
  • 6-variant access-level theory including HistoryRead-only (no Write bit)
## Summary Two-pass discovery: pass 1 browses + collects `PendingVariable` tuples (folders still register inline), pass 2 batch-reads `DataType` + `ValueRank` + `UserAccessLevel` + `Historizing` per variable in one `Session.ReadAsync`, then registers each with accurate `DriverAttributeInfo`. Replaces the prior conservative `Int32 / ViewOnly / not-array` placeholders with real metadata: - `MapUpstreamDataType` — Boolean, Int16/32/64, UInt16/32/64, Float32/Float64, String, DateTime/UtcTime; SByte+Byte widen to Int16; custom NodeIds fall back to String. - `MapAccessLevelToSecurityClass` — reads `AccessLevels.CurrentWrite` bit → `Operate` or `ViewOnly`. Uses `UserAccessLevel` (post-ACL-filter) not `AccessLevel`. - `IsArray` from `ValueRank` (−1 = scalar). `IsHistorized` from server flag — gates PR 76 history routing. **Graceful degradation**: individual attribute Bad → type default; wholesale enrichment failure → register every pending variable with fallback defaults. Browse never produces an empty address space when the first pass succeeded. ## Validation - 51/51 OpcUaClient.Tests pass (20 new mapping) - `dotnet build`: 0 errors ## Test plan - [x] 11-type standard DataType theory - [x] SByte+Byte widened to Int16 (with TODO comment for Core.Abstractions extension) - [x] UtcTime → DateTime - [x] Unknown type → String fallback - [x] 6-variant access-level theory including HistoryRead-only (no Write bit)
dohertj2 added 1 commit 2026-04-19 02:02:36 -04:00
Phase 3 PR 73 -- OPC UA Client browse enrichment (DataType + AccessLevel + ValueRank + Historizing). Before this PR discovered variables always registered with DriverDataType.Int32 + SecurityClassification.ViewOnly + IsArray=false as conservative placeholders -- correct wire-format NodeId but useless downstream metadata. PR 73 adds a two-pass browse. Pass 1 unchanged shape but now collects (ParentFolder, BrowseName, DisplayName, NodeId) tuples into a pendingVariables list instead of registering each variable inline; folders still register inline. Pass 2 calls Session.ReadAsync once with (variableCount * 4) ReadValueId entries reading DataType + ValueRank + UserAccessLevel + Historizing for every variable. Server-side chunking via the SDK keeps the request shape within the server's per-request limits automatically. Attribute mapping: MapUpstreamDataType maps every standard DataTypeIds.* to a DriverDataType -- Boolean, SByte+Byte widened to Int16 (DriverDataType has no 8-bit, flagged in comment for future Core.Abstractions widening), Int16/32/64, UInt16/32/64, Float->Float32, Double->Float64, String, DateTime+UtcTime->DateTime. Unknown/vendor-custom NodeIds fall back to String -- safest passthrough for Variant-wrapped structs/enums/extension objects since the cascading-quality path preserves upstream StatusCode+timestamps regardless. MapAccessLevelToSecurityClass reads AccessLevels.CurrentWrite bit (0x02) -- when set, the variable is writable-for-this-user so it surfaces as Operate; otherwise ViewOnly. Uses UserAccessLevel not AccessLevel because UserAccessLevel is post-ACL-filter -- reflects what THIS session can actually do, not the server's default. IsArray derived from ValueRank (-1 = scalar, 0 = 1-D array, 1+ = multi-dim). IsHistorized reflects the server's Historizing flag directly so PR 76's IHistoryProvider routing can gate on it. Graceful degradation: (a) individual attribute failures (Bad StatusCode on DataType read) fall through to the type defaults, variable still registers; (b) wholesale enrichment-read failure (e.g. session dropped mid-browse) catches the exception, registers every pending variable with fallback defaults via RegisterFallback, browse completes. Either way the downstream address space is never empty when browse succeeded the first pass -- partial metadata is strictly better than missing variables. Unit tests (OpcUaClientAttributeMappingTests, 20 facts): MapUpstreamDataType theory covers 11 standard types including Boolean/Int16/UInt16/Int32/UInt32/Int64/UInt64/Float/Double/String/DateTime; separate facts for SByte+Byte (widened to Int16), UtcTime (DateTime), custom NodeId (String fallback); MapAccessLevelToSecurityClass theory covers 6 access-level bitmasks including CurrentRead-only (ViewOnly), CurrentWrite-only (Operate), read+write (Operate), HistoryRead-only (ViewOnly -- no Write bit). 51/51 OpcUaClient.Tests pass (31 prior + 20 new). dotnet build clean. Pending variables structured as a private readonly record struct so the ref-type allocation is stack-local for typical browse sizes. Paves the way for PR 74 SessionReconnectHandler (same enrichment path is re-runnable on reconnect) + PR 76 IHistoryProvider (gates on IsHistorized). 28328def5d
dohertj2 merged commit 8cd932e7c9 into v2 2026-04-19 02:02:40 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#72