Three fixes for the SubscriptionManager/MxAccessClient subscription pipeline:
1. Serialize Subscribe and UnsubscribeClient with a SemaphoreSlim gate to prevent
race where old-session unsubscribe removes new-session COM subscriptions.
CreateMxAccessSubscriptionsAsync is now awaited instead of fire-and-forget.
2. Fix dual VTQ delivery in MxAccessClient.OnDataChange — each update was delivered
twice (once via stored callback, once via OnTagValueChanged property). Now uses
stored callback as the single delivery path.
3. Store pending tag addresses when CreateMxAccessSubscriptionsAsync fails (MxAccess
down) and retry them on reconnect via NotifyReconnection/RetryPendingSubscriptionsAsync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move connection bindings, attribute overrides, and area assignment from
inline expandable rows on the Instances table to a separate page at
/deployment/instances/{id}/configure for a cleaner, less cramped UX.
Replace raw dictionary casting with ScriptParameters wrapper that provides
Get<T>, Get<T?>, Get<T[]>, and Get<List<T>> with clear error messages,
numeric conversion, and JsonElement support for Inbound API parameters.
The LmxProxy client's ExtractArrayValue now returns proper .NET arrays
(bool[], int[], DateTime[], etc.) instead of ArrayValue objects. Removed
the reflection-based FormatArrayContainer logic — IEnumerable handling
is sufficient for all array types.
Added DatetimeArray message (repeated int64, UTC ticks) to proto and
code-first contracts. Host serializes DateTime[] → DatetimeArray.
Client deserializes DatetimeArray → DateTime[] (not raw long[]).
Client ExtractArrayValue now unpacks all array types including DateTime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DateTime[] from MxAccess was falling through to ToString() fallback,
producing "System.DateTime[]" instead of actual values. Now converts
each DateTime to UTC ticks and stores in Int64Array.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArrayValue from LmxProxy client was showing as type name in debug views.
Added ValueFormatter utility and NormalizeValue in LmxProxyDataConnection
to convert arrays at the adapter boundary. DateTime arrays remain as
"System.DateTime[]" due to server-side v1 string serialization.
Docker: include lmxproxy/src/ZB.MOM.WW.LmxProxy.Client in build context
so the project reference resolves during container image build.
CLI: fix set-bindings JSON parsing — use JsonElement.GetString()/GetInt32()
instead of object.ToString() which returned null for deserialized elements.
Thread backup data connection fields through management command messages,
ManagementActor handlers, SiteService, site-side SQLite storage, and
deployment/replication actors. The old --configuration CLI flag is kept
as a hidden alias for backwards compatibility.
Add ActiveEndpoint field to DataConnectionHealthReport showing which
endpoint is active (Primary, Backup, or Primary with no backup configured).
Log failover transitions and connection restoration events to the site
event log via ISiteEventLogger, passed as an optional parameter through
the actor hierarchy for backwards compatibility.
Update CreateConnectionCommand to carry PrimaryConnectionDetails,
BackupConnectionDetails, and FailoverRetryCount. Update all callers:
DataConnectionManagerActor, DataConnectionActor, DeploymentManagerActor,
FlatteningService, and ConnectionConfig. The actor stores both configs
but continues using primary only — failover logic comes in Task 3.
Covers entity model, failover state machine, health reporting,
UI/CLI changes, and deployment flow for optional backup endpoints
with automatic failover after configurable retry count.
Switches from v1 string-based proto stubs to the production LmxProxyClient
(v2 native TypedValue protocol) via project reference. Deletes 6k+ lines of
generated proto code. Preserves ILmxProxyClient adapter interface for testability.
LmxFakeProxy is no longer needed now that two real LmxProxy v2 instances
are available for testing. Added remote test infra section to test_infra.md
documenting the windev instances. Removed tagsim (never committed).
Both instances share API keys and connect to the same AVEVA platform.
Verified: 17/17 integration tests pass against both instances.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AppEngine built-in tag is always present and constantly updating (~1s),
making it a more reliable probe than a user-deployed TestChildObject tag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gap 1: Active health probing verified — 60s recovery after platform restart.
Gap 2: Address-based subscription cleanup — no stale handles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tested aaBootstrap kill on windev — three gaps identified:
1. No active health probing (IsConnected stays true on dead connection)
2. Stale SubscriptionManager handles after reconnect cycle
3. AVEVA objects don't auto-start after platform crash (platform behavior)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MxStatusMapper: maps all 40+ MxStatusDetail codes, MxStatusCategory,
and MxStatusSource to human-readable names and client messages
- OnDataChange: checks MXSTATUS_PROXY.success and overrides quality with
specific OPC UA code when MxAccess reports a failure (e.g., CommFailure,
ConfigError, WaitingForInitialData)
- OnWriteComplete: uses MxStatusMapper.FormatStatus for structured logging
- Write errors: catches COMException separately with HRESULT in message
- Read errors: distinguishes COM, timeout, and generic failures in logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents when the full STA+Application.Run() approach is needed
(secured/verified writes), why our first attempt failed, the correct
pattern using Form.BeginInvoke(), and tradeoffs vs fire-and-forget.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MxAccess docs explicitly state OnWriteComplete always fires after Write().
The real cause is no Windows message pump in the headless service process to
marshal the COM callback. Fire-and-forget is safe for supervisory writes but
would miss secured/verified write rejections (errors 1012/1013).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SubscriptionManager tracked client-to-tag routing but never called
MxAccessClient.SubscribeAsync to create the actual COM subscriptions,
so OnDataChange never fired. Now creates MxAccess subscriptions for
new tags and disposes them when the last client unsubscribes.
All 17 integration tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replaced STA dispatch thread with Task.Run pattern for COM interop
- Fixed TypedValue oneof tracking with property-level _setCase field
- Added x-api-key DelegatingHandler for gRPC metadata authentication
- Fixed CheckApiKey RPC to validate request body key (not header)
- Integration tests: 15/17 pass (reads, subscribes, API keys, connections)
- 2 write tests pending (OnWriteComplete callback timing issue)
- v2 service deployed on windev port 50100
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace JoeAppEngine tags with TestChildObject tags (TestBool, TestInt, TestFloat,
TestDouble, TestString, TestDateTime, and array variants) in Phase 4 and Phase 7
plans. These tags cover all TypedValue oneof cases for comprehensive v2 testing.
Design doc covers architecture, v2 protocol (TypedValue/QualityCode), COM threading
model, session lifecycle, subscription semantics, error model, and guardrails.
Implementation plans are detailed enough for autonomous Claude Code execution.
Verified all dev tooling on windev (Grpc.Tools, protobuf-net.Grpc, Polly v8, xUnit).
Generate high-level requirements and 10 component documents derived from source code
and protocol specs. Uses lmxproxy_updates.md (v2 TypedValue/QualityCode) as the source
of truth, with v1 string-based encoding documented as legacy context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace SiteDataConnectionAssignment join table with a direct SiteId FK on DataConnection,
simplifying the data model, repositories, UI, CLI, and deployment service.