Compare commits

...

26 Commits

Author SHA1 Message Date
2baca785ad Merge pull request 'abstractions+driver+client.shared: extend AlarmEventArgs with rich payload (PR E.7)' (#415) from track-e7-alarm-event-args-extension into master 2026-04-30 17:49:19 -04:00
Joseph Doherty
1d62709060 abstractions+driver+client.shared: extend AlarmEventArgs with rich payload (PR E.7)
Fourteenth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver
implements IAlarmSource, merged) and B.3 (DriverNodeManager prefers
driver-native ack, merged).

Three new optional fields on Core.Abstractions.AlarmEventArgs:

- OperatorComment — populated by the driver-native gateway path on
  Acknowledge transitions. Null on raise / clear, and null on the
  sub-attribute fallback path where the comment collapses into a
  single string write.
- OriginalRaiseTimestampUtc — preserved across Acknowledge so OPC
  UA Part 9 conditions keep the original raise time.
- AlarmCategory — taxonomy bucket from the upstream alarm system.
  Maps to ConditionClassName downstream when a class mapping is
  configured.

GalaxyDriver.OnPumpAlarmTransition populates the new fields from
GalaxyAlarmTransition (PR B.1). Empty strings collapse to null so
consumers can use is-null rather than is-null-or-empty checks.

Client.Shared mirror DTO (Client.Shared/Models/AlarmEventArgs)
gains the same three properties so the Client.UI / Client.CLI
surfaces can reflect the rich payload — the actual UI/CLI
verbose-output and Show-Details rendering ship as a follow-up
PR; this PR locks in the payload contract.

Tests:
- 2 new tests in Driver.Galaxy.Tests pin the populated-vs-null
  behaviour for full-payload Acknowledge and bare-bones Raise
  transitions respectively.
- Solution build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:46:47 -04:00
0b5a4a676e Merge pull request 'server: DriverNodeManager prefers IAlarmSource ack over IWritable (PR B.3)' (#414) from track-b3-prefer-driver-native-alarm into master 2026-04-30 17:23:09 -04:00
Joseph Doherty
edc984987b server: DriverNodeManager prefers IAlarmSource ack over IWritable (PR B.3)
Thirteenth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver
implements IAlarmSource, merged).

When DriverNodeManager registers an AlarmConditionState with
AlarmConditionService, it now picks the acknowledger:

- Driver implements IAlarmSource → DriverAlarmSourceAcknowledger
  routes the operator comment through IAlarmSource.AcknowledgeAsync
  via the existing AlarmSurfaceInvoker (Phase 6.1 resilience pipeline,
  no-retry per decision #143). Preserves operator-comment fidelity
  end-to-end — the value-driven sub-attribute write collapses the
  comment into a single string write that loses MxAccess metadata.
- Driver does not implement IAlarmSource →
  DriverWritableAcknowledger fallback (existing behaviour for
  AbCip / Modbus / S7 / etc).

The dedup logic that prefers driver-native transitions over
sub-attribute synthesis lives in AlarmConditionService and is
already in place — drivers that surface OnAlarmEvent (B.2) feed
the service directly, while sub-attribute writes still flow
through DriverNodeManager's ConditionSink so a Galaxy template
without $Alarm extensions stays functional.

Tests:
- 2 new routing-decision tests in
  DriverAlarmSourceAcknowledgerRoutingTests pin the
  IAlarmSource detection used at registration time.
- Server build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:20:45 -04:00
6126374594 Merge pull request 'driver-galaxy: GalaxyDriver implements IAlarmSource (PR B.2)' (#413) from track-b2-galaxy-driver-ialarmsource into master 2026-04-30 17:18:20 -04:00
Joseph Doherty
38afc234ff driver-galaxy: GalaxyDriver implements IAlarmSource (PR B.2)
Twelfth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.1 (EventPump
dispatch, merged) and PR E.2 (.NET SDK alarm methods, merged).
Restores the v1 IAlarmSource capability that PR 7.2 retired with the
legacy Galaxy.Host / Galaxy.Proxy projects.

GalaxyDriver gains:
- IAlarmSource on the class declaration → eight capabilities total
  (IDriver / ITagDiscovery / IReadable / IWritable / ISubscribable /
  IRediscoverable / IHostConnectivityProbe / IAlarmSource).
- SubscribeAlarmsAsync — returns a sentinel handle and starts the
  shared EventPump (alarm wiring is lazy on first sub).
  Multiple handles share the same gateway stream; the server-side
  AlarmConditionService dispatches per-source-node downstream.
- UnsubscribeAlarmsAsync — symmetric handle removal; rejects
  handles not issued by this driver.
- AcknowledgeAsync — issues one gateway RPC per acknowledgement
  through IGalaxyAlarmAcknowledger. ConditionId carries the alarm
  full reference; falls back to SourceNodeId when empty.
- OnAlarmEvent — bridges EventPump.OnAlarmTransition (B.1) onto
  AlarmEventArgs. Suppressed when no alarm subscription is active so
  untracked transitions don't leak through.

New runtime types:
- IGalaxyAlarmAcknowledger — test seam.
- GatewayGalaxyAlarmAcknowledger — production wrapper around
  MxGatewayClient.AcknowledgeAlarmAsync (PR E.2). Maps native
  MxStatus failures to a logged warning rather than a thrown
  exception so a transient MxAccess hiccup doesn't fail the
  operator's Acknowledge.
- GalaxyAlarmSubscriptionHandle — driver-side IAlarmSubscriptionHandle.

Production runtime construction in BuildProductionRuntimeAsync wires
the acknowledger when not pre-injected; tests inject a fake via the
internal ctor.

Tests:
- 7 new tests in GalaxyDriverAlarmSourceTests — subscribe → event
  fire path, suppress without subscription, unsubscribe stops flow,
  foreign-handle rejection, ack routes per-request, ack falls back
  to SourceNodeId, ack throws NotSupported without acknowledger.
- Full Driver.Galaxy.Tests: 203 passed (was 196; 7 new).

Operates as a "stub-ready" surface — runtime ack calls will return
PERMISSION_DENIED until A.3 ships the gateway-side dispatch, and no
alarm transitions will arrive until A.2 adds the worker MxAccess
subscription. Both will activate this code path automatically when
the gateway side lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:15:46 -04:00
95422995c0 Merge pull request 'server: Phase7Composer accepts DI-registered IAlarmHistorianWriter (PR B.4)' (#412) from track-b4-sidecar-alarm-historian-writer into master 2026-04-30 16:33:27 -04:00
Joseph Doherty
6e282b9946 server: Phase7Composer accepts DI-registered IAlarmHistorianWriter (PR B.4)
Sixth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR C.2 (sidecar
serves IAlarmEventWriter when enabled), already merged.

Today Phase7Composer.ResolveHistorianSink only scans drivers for an
IAlarmHistorianWriter — no Galaxy driver provides one since PR 7.2,
so the resolution falls through to NullAlarmHistorianSink and
scripted-alarm transitions are silently discarded.

WonderwareHistorianClient already implements IAlarmHistorianWriter
and Program.cs:178 already registers it as a singleton when
Historian:Wonderware:Enabled=true. The gap was that Phase7Composer
ignored DI: this PR adds an optional injectedWriter constructor
parameter, and ASP.NET Core DI resolves it from the same
registration when present.

- Phase7Composer constructor: new optional IAlarmHistorianWriter?
  injectedWriter parameter (default null). Backward-compatible —
  existing callers don't need to change; DI populates it
  automatically when the singleton is registered.
- New static SelectAlarmHistorianWriter helper — resolution order
  is driver → DI → null. Drivers win when both are present so a
  future GalaxyDriver-as-IAlarmHistorianWriter takes the write
  path directly, preserving the v1 invariant where a driver that
  natively owns the historian client doesn't bounce through the
  sidecar IPC.
- ResolveHistorianSink uses the helper + emits a structured log
  line identifying which source provided the writer.

Tests:
- 4 SelectAlarmHistorianWriter precedence tests — no source / DI
  only / driver wins over DI / first-driver-with-writer wins.
- Pre-existing 4 HostStatusPublisherTests SQL failures unrelated
  to this change (require the docker-host SQL Server at
  10.100.0.35,14330 per CLAUDE.md). Phase7 + alarm tests all
  green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:31:00 -04:00
f67b3b1b30 Merge pull request 'sidecar: wire IAlarmEventWriter into Program.cs (PR C.2)' (#411) from track-c2-program-wires-alarm-writer into master 2026-04-30 16:22:36 -04:00
Joseph Doherty
ffacbe0370 sidecar: wire IAlarmEventWriter into Program.cs (PR C.2)
Fifth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR C.1
(AahClientManagedAlarmEventWriter), already merged.

Today HistorianFrameHandler is constructed at Program.cs line 57
without an alarmWriter, so every WriteAlarmEvents frame replies
"Sidecar not configured with an alarm-event writer" and the lmxopcua
side keeps the row queued. C.2 wires a real writer behind a new
OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED toggle.

- Program.BuildAlarmWriter — gated on the env var (default true,
  fail-open under accidental misconfiguration). Constructs an
  AahClientManagedAlarmEventWriter wrapping a
  SdkAlarmHistorianWriteBackend with the same connection config the
  read path uses.
- Install-Services.ps1 — appends OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true
  to the OtOpcUaWonderwareHistorian service env block when the
  sidecar is installed. Read-only deployments flip it to false at
  service-config edit time without re-installing.
- HistorianFrameHandler signature already accepts
  IAlarmEventWriter? — supplying non-null at line 57 lights up
  the WriteAlarmEvents reply path that's been dormant since PR 3.3.

Until PR D.1 pins the live aahClientManaged entry point, the
SdkAlarmHistorianWriteBackend reports RetryPlease for every event
with a structured diagnostic. The lmxopcua-side
SqliteStoreAndForwardSink retains queued events; same effective
behaviour as today's NullAlarmHistorianSink fallback but with
visible diagnostics rather than silent discard.

Tests:
- 6 BuildAlarmWriter env-var cases — unset / true / false /
  unrecognized → default-on / capitalization variants.
- Full sidecar test suite: 56 passed (was 48; 8 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:20:11 -04:00
8a4526a376 Merge pull request 'sidecar: AahClientManagedAlarmEventWriter implements IAlarmEventWriter (PR C.1)' (#410) from track-c1-aah-alarm-writer into master 2026-04-30 16:19:36 -04:00
Joseph Doherty
f99cf5033a sidecar: AahClientManagedAlarmEventWriter implements IAlarmEventWriter (PR C.1)
Fourth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Independent of Tracks A and B —
the sidecar slot defined in HistorianFrameHandler line 242 is unwired
today; PR C.2 (next) flips it on in Program.cs.

- AlarmHistorianWriteOutcome (sidecar-local, net48 — twin of
  Core.AlarmHistorian.HistorianWriteOutcome which is net10): Ack /
  RetryPlease / PermanentFail.
- IAlarmHistorianWriteBackend abstraction so the SDK call can be
  faked in unit tests.
- AahClientManagedAlarmEventWriter implements IAlarmEventWriter,
  delegates to the backend, maps Ack→true / Retry|Permanent→false
  for the IPC bool[] reply contract. Backend exception → whole
  batch RetryPlease (preserves the sender's queue across transients
  rather than dropping). Wrong-count return defends against a
  backend bug desyncing queue accounting.
- SdkAlarmHistorianWriteBackend — production binding skeleton.
  Reports RetryPlease for every event and logs a structured
  diagnostic until PR D.1 pins the live aahClientManaged entry
  point against the dev rig. The sender's SqliteStoreAndForwardSink
  retains queued events, mirroring today's NullAlarmHistorianSink
  behaviour but with visible diagnostics instead of silent discard.
- MapOutcome shared helper — pinned via theory tests so the D.1
  swap can change the SDK call site without reshuffling the
  HRESULT → outcome mapping.

Tests:
- 6 writer tests — empty batch / single Ack / mixed Ack-Retry-
  Permanent-Ack ordering / backend-throw → RetryPlease batch /
  cancellation propagates / wrong-count defensive degrade.
- 5 outcome theory cases — hresult 0 → Ack, malformed wins over
  hresult 0, comm error → Retry, unknown failure → Retry,
  malformed + comm → Permanent.
- Full sidecar test suite: 48 passed (was 42; 6 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:17:05 -04:00
c59bf59635 Merge pull request 'driver-galaxy: EventPump dispatches OnAlarmTransition family (PR B.1)' (#409) from track-b1-eventpump-alarm into master 2026-04-30 15:44:32 -04:00
Joseph Doherty
7853e94f4b driver-galaxy: EventPump dispatches OnAlarmTransition family (PR B.1)
Second PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR A.1 in mxaccessgw
(merged) which added the OnAlarmTransitionEvent body + family. No
runtime impact yet — the gateway doesn't emit the new family until
A.3 ships; this PR just stops dropping it on the floor.

EventPump.Dispatch becomes a switch on MxEventFamily. The new
DispatchAlarmTransition decodes the proto event, runs the raw severity
through MxAccessSeverityMapper (the same four-bucket ladder v1 used —
250/500/750/1000 boundaries per docs/v1/AlarmTracking.md), and fires
an internal OnAlarmTransition event with a GalaxyAlarmTransition
record carrying the full payload.

Body absent or transition-kind unspecified → counted via
galaxy.alarm_transitions.decoding_failures and dropped. Gateway
version skew or worker malformed event therefore degrades to "fall
back to the sub-attribute path" rather than crashing the pump.

GalaxyDriver consumes the internal event in PR B.2 (next), wrapping
it onto IAlarmSource.OnAlarmEvent. The richer fields (operator user
+ comment, original raise time, category) become visible on the OPC
UA Part 9 condition once AlarmEventArgs gets extended in E.7.

Tests:
- MxAccessSeverityMapperTests — full bucket ladder + clamp behaviour
  for negative + out-of-range inputs.
- EventPumpAlarmTests — raise/ack/clear sequence dispatches in order
  with operator metadata + original-raise preserved; unspecified
  kind drops; missing body drops; mixed data-change + alarm streams
  dispatch independently; OnWriteComplete / OperationComplete
  filtered out.

Full Driver.Galaxy.Tests suite: 196 passed (was 191 — 5 new tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:41:44 -04:00
Joseph Doherty
49ae6e7b6f docs: alarms-over-gateway — add Track E client surface refresh
Cover both client surfaces that become user-visible when the alarm
path lights up:

- mxaccessgw client SDKs in 5 languages (.NET, Python, Go, Java,
  Rust). E.1 regens proto across all of them; E.2-E.6 add per-language
  alarms helpers (subscribe / acknowledge / query-active) plus matching
  CLI verbs.
- lmxopcua OPC UA-facing clients (Client.CLI, Client.UI). E.7 extends
  AlarmEventArgs with the new optional fields, surfaces them in the
  CLI's --verbose / --json output and the UI's Show-details toggle,
  and updates ClientRequirements + Client.{CLI,UI}.md.

Sequencing: E.1 first (mechanical regen), then E.2-E.7 in parallel.
E.2 (.NET) is on the critical path because lmxopcua consumes it; the
other-language SDKs can ship asynchronously without gating D.1.

12 PRs grew to 19 total: 4 in A, 5 in B, 2 in C, 7 in E, 1 in D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:20:57 -04:00
Joseph Doherty
8d0e13e69e docs: alarms-over-gateway plan — add Track D deployment refresh
After A/B/C all merge, the running services on C:\publish need to be
refreshed before the Galaxy alarm-event family flows end-to-end. Add
PR D.1: a Refresh-Services.ps1 script + runbook for stopping in
reverse-dependency order, restaging binaries from the build outputs,
restarting in forward-dependency order, and capturing a smoke-run
artifact.

D.1 gates B.5 (docs sweep) — the documentation records the
as-deployed shape, so the deployment has to be live first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:11:23 -04:00
Joseph Doherty
7367b3e23f docs: alarm-historian write moves from gateway to historian sidecar
Revise the alarms-over-gateway plan based on review feedback:

The gateway is for MxAccess (live data + Galaxy hierarchy); the
Wonderware historian sidecar is for aahClientManaged (time-series +
alarms historian). Two SDKs, two concerns. Routing alarm-historian
write-back through the gateway would force coupling that doesn't need
to exist — the sidecar already has a dormant WriteAlarmEvents IPC slot
ready to wire.

Drop A.5 (gateway WriteHistorianEvent RPC). Add Track C — two PRs in
the historian sidecar that complete the dormant slot:
  C.1 AahClientManagedAlarmEventWriter implementation
  C.2 Program.cs wires the writer into HistorianFrameHandler

B.4 reverses from "delete the IPC slot" to "consume the IPC slot" via
a new SidecarAlarmHistorianWriter on the lmxopcua side.

Also tightens Why-section #3 + D5 to make explicit that the path is
exclusively for non-Galaxy alarm producers (scripted alarms today, AB
CIP ALMD or others future). Galaxy-native alarms reach AVEVA Historian
via System Platform's own HistorizeToAveva toggle, independent of
anything in our stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:08:58 -04:00
Joseph Doherty
65a5f64931 docs: plan — alarms over the mxaccessgw gateway
Coordinated cross-repo epic to restore the three v1 alarm capabilities
that PR 7.2 regressed: rich MxAccess alarm-event metadata, native
Acknowledge semantics, and the IAlarmHistorianWriter write-back path.

Architectural split: gateway owns MxAccess transport (new
OnAlarmTransition event family + AcknowledgeAlarm / QueryActiveAlarms /
WriteHistorianEvent RPCs); lmxopcua keeps the OPC UA Part 9 state
machine, ACL/role enforcement, and multi-source aggregation. The
existing value-driven sub-attribute path stays as fallback.

10 PRs total — 5 in mxaccessgw, 5 in lmxopcua — sequenced so each
side's work is independently reviewable. End-of-epic gate is a parity
matrix run with five new alarm scenarios.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:02:48 -04:00
Joseph Doherty
80104caf09 sidecar: switch Wonderware historian sidecar from x86 to x64
The sidecar was set to PlatformTarget=x86 + Prefer32Bit=true to mirror
v1's Driver.Galaxy.Host bitness, which itself was x86 only because of
MXAccess COM. PR 7.2 retired Galaxy.Host, so that constraint is gone.

AVEVA Historian 2020 ships an x64 build of every SDK assembly the
sidecar needs (lib\aahClientManaged.dll + aahClient.dll + aahClientCommon.dll
sourced from C:\Program Files (x86)\Wonderware\Historian\x64\; the
remaining three SDK assemblies — Historian.CBE / DPAPI /
ArchestrA.CloudHistorian.Contract — are pure-managed AnyCPU and load
in either bitness). Drop PlatformTarget to x64 on both the sidecar
project and its test project; running 37/37 historian tests + the
live install confirms the SDK loads and serves the named pipe in a
64-bit-native process.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:55:59 -04:00
Joseph Doherty
493a0ba613 build: copy Server appsettings.json to publish output
Microsoft.NET.Sdk doesn't auto-include appsettings.json the way Web SDK
does, so dotnet publish was leaving it behind. Without it next to the
EXE the Windows-service-mode host can't find Node + ConfigDb config and
the install scripts had to copy it by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:41:44 -04:00
Joseph Doherty
ea045477ad chore: drop root scratch + retired v2-mxgw plan docs
- Delete _p54.json / _p55.json (PR-body snapshots for the shipped S7
  + Mitsubishi research docs).
- Delete session.dat (38-byte CLI runtime cache, not produced by any
  current source code) and add it to .gitignore so it doesn't come
  back.
- Delete lmx_backend.md / lmx_mxgw.md / lmx_mxgw_impl.md. All three
  carried " Completed 2026-04-30" historical-record banners — the
  v2-mxgw migration shipped + merged to master, so the design plans
  served their purpose. Drop the cross-refs from CLAUDE.md and
  docs/v1/README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:47:52 -04:00
Joseph Doherty
33054c3275 docs: drop dangling FOCAS refs + link unreferenced v2 design docs
- docs/drivers/FOCAS.md and docs/v2/implementation/focas-wire-protocol.md
  pointed at focas-deployment.md and focas-simulator-plan.md, both of
  which were untracked drafts that have since been removed. Drop the
  refs (the wire-protocol companion now stands on its own; deployment
  guidance lives inline in the FOCAS driver doc).
- Link the orphan v2 design docs from docs/README.md (multi-host
  dispatch, v2 release readiness, the historical lmx-followups tracker)
  and from modbus-test-plan.md (s7.md, mitsubishi.md per-family quirk
  catalogs, sibling to dl205.md).

Surfaced by the doc audit; no content changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:42:28 -04:00
Joseph Doherty
77229dfaf3 chore: post-audit cleanup — gr/ relocated, scratch + PR-body snapshots removed
- gr/ folder moved to sibling repo at C:\Users\dohertj2\Desktop\graccess\gr;
  the SQL queries + DDL captures belong with the graccess CLI work, not
  with the OPC UA server. PR 7.2 retired direct Galaxy-DB access from this
  repo (mxaccessgw owns those queries server-side now).
- Drop the now-obsolete "Galaxy Repository Database" section in CLAUDE.md
  for the same reason — server no longer queries the DB directly.
- Delete root scratch files surfaced by the doc audit (runtimestatus.md,
  service_info.md) — abandoned plan + operational scratch.
- Delete docs/v2/implementation/pr-{1,2,4}-body.md — ephemeral PR-body
  snapshots from the v2-mxgw rollout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:36:13 -04:00
Joseph Doherty
99016c3137 docs: README — reinstate verified v2 links + flag v1 archive
Two follow-ups from the post-PR-7.2 audit:

1. Reinstate verified-current architecture deep-dive links that the
   doc-cleanup pass dropped pending verification:
   - docs/OpcUaServer.md (server composition, namespace fan-out,
     Polly invoker)
   - docs/IncrementalSync.md (driver-backend rediscovery + config
     publishes)
   - docs/ReadWriteOperations.md (driver vs virtual vs scripted-alarm
     dispatch)
   All three reference live Phase 6.2 / Phase 7 features and the
   current GenericDriverNodeManager / CapabilityInvoker / OTOPCUA0001
   analyzer codepaths.

2. Restructured the README link table into three logical sections —
   "Architecture deep-dives" / "Drivers" / "Clients" — and added a
   "v1 archive" section pointing at docs/v1/ for the retired in-process
   MXAccess docs.

3. Removed the dead docs/Configuration.md link (the file moved to
   docs/v1/Configuration.md in the v1 archive sweep). All 16 link
   targets in the new README now resolve.

Plus: physically removed the 9 leftover Driver.Galaxy.* directories
from src/ and tests/ that PR 7.2's git rm cleared from tracking but
left as orphan bin/obj scaffolding on disk. No tracked-content
change for that part.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:04:57 -04:00
Joseph Doherty
006af51768 docs: post-PR-7.2 cleanup — audit + three-track scrub
Audit (three parallel agent passes) found 43 markdown files carrying
stale references to the deleted Galaxy.Host/Proxy/Shared projects
after the v2-mxgw merge. This commit lands the prioritized fixes.

Track 1 — high-traffic in-place rewrites (3 files, ~454 lines deleted)
- README.md (202 → 91 lines): drops .NET 4.8 / x86 / TopShelf install
  text; leads with the multi-driver .NET 10 server identity and points
  at scripts/install/Install-Services.ps1 and the parity rig.
- docs/v2/driver-specs.md §1 Galaxy (~289 → ~66 lines): replaces the
  Tier-C out-of-process spec with a Tier-A in-process description
  matching the current GalaxyDriver code, with the four-section
  GalaxyDriverOptions JSON shape pulled verbatim from
  Config/GalaxyDriverOptions.cs.
- docs/drivers/Galaxy.md (211 → 92 lines): full rewrite around the
  current Browse/Runtime/Health/Config sub-folders.

Track 2 — historical banners (5 files)
- lmx_mxgw.md, lmx_mxgw_impl.md, lmx_backend.md,
  docs/v2/Galaxy.ParityMatrix.md,
  docs/v2/implementation/phase-2-galaxy-out-of-process.md each get a
  " Completed 2026-04-30 — historical record" banner block. lmx_mxgw.md
  also fixes two dead links (`docs/Galaxy.Driver.md` and
  `docs/v2/Galaxy.Driver.md`) → `docs/drivers/Galaxy.md`.

Track 3 — v1 archive sweep (10 git mv + 1 new index + 2 in-place scrubs)
- Moved 10 v1 docs under docs/v1/ preserving subpath structure:
  AlarmTracking, Configuration, DataTypeMapping, HistoricalDataAccess,
  Subscriptions (top-level); drivers/Galaxy-Repository,
  drivers/Galaxy-Test-Fixture; reqs/GalaxyRepositoryReqs,
  reqs/MxAccessClientReqs, reqs/ServiceHostReqs.
- New docs/v1/README.md is the shared archive banner + per-file table.
- docs/README.md repointed to the v1 paths and updated to reflect the
  v2 two-process deploy shape (Server + Admin + optional
  OtOpcUaWonderwareHistorian).
- docs/v2/Galaxy.ParityRig.md got a historical banner + four inline
  scrubs marking the OtOpcUaGalaxyHost service / Driver.Galaxy.Host
  EXE / Driver.Galaxy.ParityTests project as deleted-in-PR-7.2.

The repo's live-reading surface (README + CLAUDE.md + docs/v2/) now
describes only the post-PR-7.2 architecture. v1 docs are preserved as
a labelled archive under docs/v1/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:59:59 -04:00
Joseph Doherty
ae7106dfce Merge branch 'v2-mxgw-integration': in-process GalaxyDriver via mxaccessgw
Lands the v2-mxgw migration end-to-end (39 PRs across 7 phases, plus
follow-up triage). Galaxy access now flows through the in-process
GalaxyDriver talking gRPC to a separately-installed mxaccessgw,
replacing the legacy out-of-process Galaxy.Host / Galaxy.Proxy /
Galaxy.Shared trio. The OtOpcUa server is .NET 10 AnyCPU; the
MXAccess COM bitness constraint moved to the gateway's worker.

Headline changes:

- Phase 1 (1.1-1.3, 1+2.W): IHistoryRouter at the server level;
  per-driver IHistoryProvider fallback retired.
- Phase 2 (2.1-2.3): AlarmConditionService at the server level driven
  by AlarmConditionInfo's five sub-attribute refs (InAlarmRef /
  PriorityRef / DescAttrNameRef / AckedRef / AckMsgWriteRef).
- Phase 3 (3.1-3.W): Driver.Historian.Wonderware sidecar (net48 x86)
  + .NET 10 client + pipe IPC for the historian SDK.
- Phase 4 (4.0-4.W): in-process Driver.Galaxy with all 8 capability
  interfaces (ITagDiscovery / IReadable / IWritable / ISubscribable /
  IRediscoverable / IHostConnectivityProbe + IDriver / IDisposable);
  ReconnectSupervisor + DeployWatcher + PerPlatformProbeWatcher.
- Phase 5 (5.1-5.W): parity matrix scaffolding; matrix verified green
  on the live ZB galaxy 2026-04-30 (14 passed / 1 skipped / 0 failed).
- Phase 6 (6.1-6.W): perf surface — OpenTelemetry traces around gw
  calls, bounded EventPump channel + drop-newest metrics, buffered
  update interval landing, soak scenario harness, tuned defaults,
  Galaxy.Performance.md.
- Phase 7 (7.1-7.3): Galaxy:DefaultBackend = "GalaxyMxGateway"
  default-flip; PR 7.2 deleted the 9 legacy project directories
  (Driver.Galaxy.Host, .Proxy, .Shared, Galaxy.E2E, Galaxy.ParityTests,
  Galaxy.TestSupport, plus the three tests projects); doc + memory
  housekeeping.

Plus follow-ups: production-path read via subscribe-once, ApiKey
resolver (env:/file:/literal), session-level
SetBufferedUpdateInterval, EventPump channel capacity surfaced through
options. graccess-cli typelib + lifecycle bugs filed as separate
requirements docs in the gw repo.
2026-04-30 08:19:06 -04:00
251 changed files with 3471 additions and 9416 deletions

3
.gitignore vendored
View File

@@ -37,3 +37,6 @@ src/ZB.MOM.WW.OtOpcUa.Server/config_cache.db
# E2E sidecar config — NodeIds are specific to each dev's local seed (see scripts/e2e/README.md)
scripts/e2e/e2e-config.json
config_cache*.db
# Client CLI/UI runtime scratch (last-connected endpoint cache)
session.dat

View File

@@ -16,8 +16,7 @@ in this repo is .NET 10. PR 7.2 retired the legacy in-process
`Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared` projects + the
`OtOpcUaGalaxyHost` Windows service.
See `lmx_mxgw.md` for the migration design and
`docs/v2/Galaxy.Performance.md` for the runtime perf surface
See `docs/v2/Galaxy.Performance.md` for the runtime perf surface
(tracing, metrics, soak harness).
## Architecture Overview
@@ -58,19 +57,6 @@ Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double,
The gateway lives in a sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`. See `docs/v2/Galaxy.ParityRig.md` for the gw setup recipe (build, API key provisioning via `apikey create-key`, env-var overrides for HTTP/2 cleartext + worker path). The gw's MXAccess Toolkit reference (its `gateway.md`) is the canonical MxAccess API doc; the standalone `mxaccess_documentation.md` previously kept in this repo retired in PR 7.3.
## Galaxy Repository Database
Connection: `sqlcmd -S localhost -d ZB -E` (Windows Auth). See `gr/connectioninfo.md`.
The `gr/` folder contains:
- `queries/` — SQL for hierarchy extraction, attribute lookup, and change detection
- `ddl/tables/` and `ddl/views/` — Schema definitions
- `schema.md` — Full table/view reference
- `build_layout_plan.md` — Step-by-step plan for building the OPC UA address space from DB queries
- `gr/CLAUDE.md` — Detailed guidance for working within the `gr/` subfolder
Key tables: `gobject` (hierarchy/deployment), `template_definition` (object categories), `dynamic_attribute` (user-defined attributes), `primitive_instance` (primitive-to-attribute links), `galaxy` (change detection).
## Build Commands
```bash

251
README.md
View File

@@ -1,200 +1,115 @@
# LmxOpcUa
# OtOpcUa
OPC UA server and cross-platform client tools for AVEVA System Platform (Wonderware) Galaxy. The server exposes Galaxy tags via MXAccess as an OPC UA address space. The client stack provides a shared library, CLI tool, and Avalonia desktop application for browsing, reading/writing, subscriptions, alarms, and historical data.
OPC UA server (.NET 10 AnyCPU) that exposes a fleet of industrial drivers as a single OPC UA address space. Drivers ship in-process for AVEVA System Platform Galaxy (via the sibling `mxaccessgw` repo), Modbus TCP, Siemens S7, Allen-Bradley CIP (ControlLogix / CompactLogix), Allen-Bradley Legacy (SLC 500 / MicroLogix), Beckhoff TwinCAT (ADS), FANUC FOCAS, and OPC UA Client (gateway).
A cross-platform client stack (.NET 10) — shared library, CLI, and Avalonia desktop app — connects to any OPC UA server.
## Architecture
```
OPC UA Clients
(CLI, Desktop UI, 3rd-party)
|
v
+-----------------+ +------------------+ +-----------------+
| Galaxy Repo DB |---->| OPC UA Server |<--->| MXAccess Client |
| (SQL Server) | | (address space) | | (STA + COM) |
+-----------------+ +------------------+ +-----------------+
| |
+-------+--------+ +---------+---------+
| Status Dashboard| | Historian Runtime |
| (HTTP/JSON) | | (SQL Server) |
+----------------+ +-------------------+
OPC UA Clients (CLI, Desktop UI, 3rd-party)
|
v
+-------------------------------------+
| OtOpcUa.Server (.NET 10 AnyCPU) |
| address space + capability fan-out|
+-------------------------------------+
| | | | | | | |
Galaxy Modbus S7 AbCip AbLeg TwinCAT FOCAS OpcUaClient
|
v
mxaccessgw (sibling repo, gRPC)
|
v
MXAccess COM (x86 worker, on AVEVA box)
```
## Contained Name vs Tag Name
Galaxy is the only driver with an external runtime: it speaks gRPC to a separately installed `mxaccessgw` server (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`) which owns the MXAccess COM apartment and the x86/STA bitness constraint server-side. Everything in this repo is platform-agnostic .NET 10.
| Browse Path (contained names) | Runtime Reference (tag name) |
|-------------------------------|------------------------------|
| `TestMachine_001/DelmiaReceiver/DownloadPath` | `DelmiaReceiver_001.DownloadPath` |
| `TestMachine_001/MESReceiver/MoveInBatchID` | `MESReceiver_001.MoveInBatchID` |
## Prerequisites
---
- .NET 10 SDK (server, drivers, clients all target .NET 10)
- SQL Server reachable for the central config DB
- For Galaxy specifically: a running `mxaccessgw` deployment — see [docs/v2/Galaxy.ParityRig.md](docs/v2/Galaxy.ParityRig.md)
- For Wonderware Historian read-back: optional `OtOpcUaWonderwareHistorian` sidecar — see [docs/ServiceHosting.md](docs/ServiceHosting.md)
## Server
The OPC UA server runs on .NET Framework 4.8 (x86) and bridges the Galaxy runtime to OPC UA clients.
### Server Prerequisites
- .NET Framework 4.8 SDK
- AVEVA System Platform with ArchestrA Framework installed
- Galaxy repository database (SQL Server, Windows Auth)
- MXAccess COM registered (`LMXProxy.LMXProxyServer`)
- Wonderware Historian (optional, for historical data access)
- Windows (required for COM interop and MXAccess)
### Build and Run Server
## Quick Start
```bash
dotnet restore ZB.MOM.WW.LmxOpcUa.slnx
dotnet build src/ZB.MOM.WW.LmxOpcUa.Host
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Host
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
dotnet build ZB.MOM.WW.OtOpcUa.slnx
dotnet test ZB.MOM.WW.OtOpcUa.slnx
# Run the server in dev (foreground)
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server
```
The server starts on `opc.tcp://localhost:4840/LmxOpcUa` with the `None` security profile by default. Configure `Security.Profiles` in `appsettings.json` to enable `Basic256Sha256-Sign` or `Basic256Sha256-SignAndEncrypt` for transport security. See [Security Guide](docs/security.md).
The server starts on `opc.tcp://localhost:4840` with the `None` security profile. Configure `Security.Profiles` in `src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` to enable `Basic256Sha256-Sign` or `Basic256Sha256-SignAndEncrypt`. See [docs/security.md](docs/security.md).
### Install as Windows Service
## Install as Windows Services
Production deployment is driven by `scripts/install/Install-Services.ps1`, which registers the `OtOpcUa` server service (and optionally the `OtOpcUaWonderwareHistorian` sidecar) under a chosen service account. Galaxy support requires a separately installed `mxaccessgw` — neither this repo nor the install script provisions it.
```powershell
.\scripts\install\Install-Services.ps1 `
-InstallRoot 'C:\Program Files\OtOpcUa' `
-ServiceAccount 'DOMAIN\svc-otopcua'
```
Add `-InstallWonderwareHistorian` for the historian sidecar. See the script header and [docs/ServiceHosting.md](docs/ServiceHosting.md) for full options.
## Client CLI
```bash
cd src/ZB.MOM.WW.LmxOpcUa.Host/bin/Debug/net48
ZB.MOM.WW.LmxOpcUa.Host.exe install
ZB.MOM.WW.LmxOpcUa.Host.exe start
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
```
**Service logon requirement:** The service must run under a Windows account that has access to the AVEVA Galaxy and Historian. The default `LocalSystem` account can connect to MXAccess and SQL Server but **cannot authenticate with the Historian SDK** (HCAP). Configure the service to "Log on as" a domain or local user that is a recognized ArchestrA platform user. This can be set in `services.msc` or during install with `ZB.MOM.WW.LmxOpcUa.Host.exe install -username DOMAIN\user -password ***`.
### Run Server Tests
```bash
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests
```
---
## Client Stack
The client stack is cross-platform (.NET 10) and consists of three projects sharing a common `IOpcUaClientService` abstraction. No AVEVA software or COM is required — the clients connect to any OPC UA server.
### Client Prerequisites
- .NET 10 SDK
- No platform-specific dependencies (runs on Windows, macOS, Linux)
### Build All Clients
```bash
dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.Shared
dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.CLI
dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.UI
```
### Run Client Tests
```bash
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests
```
### Client CLI
```bash
# Connect
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840/LmxOpcUa
# Browse Galaxy hierarchy
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=ZB" -r -d 5
# Read a tag
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001.MachineID"
# Write a tag
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestChildObject.TestString" -v "Hello"
# Subscribe to changes
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestChildObject.TestInt" -i 500
# Read historical data
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001.TestHistoryValue" --start "2026-03-25" --end "2026-03-30"
# Subscribe to alarm events
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- alarms -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001" --refresh
# Query redundancy state
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- redundancy -u opc.tcp://localhost:4840/LmxOpcUa
```
### Client UI
```bash
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.UI
```
The desktop application provides browse tree, subscriptions, alarm monitoring, history reads, and write dialogs. See [Client UI Documentation](docs/Client.UI.md) for details.
---
## Project Structure
```
src/
ZB.MOM.WW.LmxOpcUa.Host/ OPC UA server (.NET Framework 4.8, x86)
Configuration/ Config binding and validation
Domain/ Interfaces, DTOs, enums, mappers
Historian/ Wonderware Historian data source
Metrics/ Performance tracking (rolling P95)
MxAccess/ STA thread, COM interop, subscriptions
GalaxyRepository/ SQL queries, change detection
OpcUa/ Server, node manager, address space, alarms, diff
Status/ HTTP dashboard, health checks
ZB.MOM.WW.LmxOpcUa.Client.Shared/ Shared OPC UA client library (.NET 10)
ZB.MOM.WW.LmxOpcUa.Client.CLI/ Command-line client (.NET 10)
ZB.MOM.WW.LmxOpcUa.Client.UI/ Avalonia desktop client (.NET 10)
tests/
ZB.MOM.WW.LmxOpcUa.Tests/ Server unit + integration tests
ZB.MOM.WW.LmxOpcUa.IntegrationTests/ Server integration tests (live DB)
ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests/ Shared library tests
ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests/ CLI command tests
ZB.MOM.WW.LmxOpcUa.Client.UI.Tests/ UI ViewModel + headless tests
gr/ Galaxy repository docs, SQL queries, schema
```
See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md).
## Documentation
### Server
### Architecture deep-dives
| Component | Description |
| Topic | Doc |
|---|---|
| [OPC UA Server](docs/OpcUaServer.md) | Endpoint, sessions, security policy, server lifecycle |
| [Address Space](docs/AddressSpace.md) | Hierarchy nodes, variable nodes, primitive grouping, NodeId scheme |
| [Galaxy Repository](docs/GalaxyRepository.md) | SQL queries, deployed package chain, change detection |
| [MXAccess Bridge](docs/MxAccessBridge.md) | STA thread, COM interop, subscriptions, reconnection |
| [Data Type Mapping](docs/DataTypeMapping.md) | Galaxy to OPC UA types, arrays, security classification |
| [Read/Write Operations](docs/ReadWriteOperations.md) | Value reads, writes, access level enforcement, array element writes |
| [Subscriptions](docs/Subscriptions.md) | Ref-counted MXAccess subscriptions, data change dispatch |
| [Alarm Tracking](docs/AlarmTracking.md) | AlarmConditionState nodes, InAlarm monitoring, event reporting |
| [Historical Data Access](docs/HistoricalDataAccess.md) | Historian data source, HistoryReadRaw, HistoryReadProcessed |
| [Incremental Sync](docs/IncrementalSync.md) | Diff computation, subtree teardown/rebuild, subscription preservation |
| [Configuration](docs/Configuration.md) | appsettings.json binding, feature flags, validation |
| [Status Dashboard](docs/StatusDashboard.md) | HTTP server, health checks, metrics reporting |
| [Service Hosting](docs/ServiceHosting.md) | TopShelf, startup/shutdown sequence, error handling |
| [Security](docs/security.md) | Transport security profiles, certificate trust, production hardening |
| [Redundancy](docs/Redundancy.md) | Non-transparent warm/hot redundancy, ServiceLevel, paired deployment |
| OPC UA server composition, namespace fan-out, Polly invoker | [docs/OpcUaServer.md](docs/OpcUaServer.md) |
| Address space layout | [docs/AddressSpace.md](docs/AddressSpace.md) |
| Read / Write dispatch (driver vs virtual vs scripted-alarm) | [docs/ReadWriteOperations.md](docs/ReadWriteOperations.md) |
| Incremental sync (driver-backend rediscovery + config publishes) | [docs/IncrementalSync.md](docs/IncrementalSync.md) |
| Service hosting (Server + Admin + optional historian sidecar) | [docs/ServiceHosting.md](docs/ServiceHosting.md) |
| Security (transport, LDAP, certificates) | [docs/security.md](docs/security.md) |
| Redundancy | [docs/Redundancy.md](docs/Redundancy.md) |
| Status dashboard | [docs/StatusDashboard.md](docs/StatusDashboard.md) |
### Client
### Drivers
| Component | Description |
| Topic | Doc |
|---|---|
| [Client CLI](docs/Client.CLI.md) | Connect, browse, read, write, subscribe, historyread, alarms, redundancy commands |
| [Client UI](docs/Client.UI.md) | Avalonia desktop client: browse, subscribe, alarms, history, write values |
| Driver specs (per-driver capability surface, config, addressing) | [docs/v2/driver-specs.md](docs/v2/driver-specs.md) |
| Galaxy driver | [docs/drivers/Galaxy.md](docs/drivers/Galaxy.md) |
| Modbus / S7 / AbCip / AbLegacy / TwinCAT / FOCAS / OpcUaClient | [docs/drivers/](docs/drivers/) |
| Galaxy parity rig (mxaccessgw setup) | [docs/v2/Galaxy.ParityRig.md](docs/v2/Galaxy.ParityRig.md) |
| Galaxy performance + tracing | [docs/v2/Galaxy.Performance.md](docs/v2/Galaxy.Performance.md) |
### Reference
### Clients
- [Galaxy Repository Queries](gr/CLAUDE.md) — SQL queries for hierarchy, attributes, and change detection
- [Data Type Mapping](gr/data_type_mapping.md) — Galaxy to OPC UA type mapping with security classification
| Topic | Doc |
|---|---|
| Client CLI | [docs/Client.CLI.md](docs/Client.CLI.md) |
| Client UI (Avalonia desktop) | [docs/Client.UI.md](docs/Client.UI.md) |
### v1 archive
The original v1 in-process MXAccess docs (Galaxy.Host topology,
Configuration env vars, AlarmTracking, DataTypeMapping,
HistoricalDataAccess, Subscriptions, etc.) are preserved under
[docs/v1/](docs/v1/) — historical reference only. PR 7.2 retired the
v1 architecture on 2026-04-30; current state is documented in the
sections above.
## License

View File

@@ -1 +0,0 @@
{"title":"Phase 3 PR 54 -- Siemens S7 Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/s7.md` (485 lines) covering Siemens SIMATIC S7 family Modbus TCP behavior. Mirrors the `docs/v2/dl205.md` template for future per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **No fixed memory map** — every S7 Modbus server is user-wired via `MB_SERVER`/`MODBUSCP`/`MODBUSPN` library blocks. Driver must accept per-site config, not assume a vendor layout.\n- **MB_SERVER requires non-optimized DBs** (STATUS `0x8383` if optimized). Most common field bug.\n- **Word order default = ABCD** (opposite of DL260). Driver's S7 profile default must be `ByteOrder.BigEndian`, not `WordSwap`.\n- **One port per MB_SERVER instance** — multi-client requires parallel FBs on 503/504/… Most clients assume port 502 multiplexes (wrong on S7).\n- **CP 343-1 Lean is server-only**, requires the `2XV9450-1MB00` license.\n- **FC20/21/22/23/43 all return Illegal Function** on every S7 variant — driver must not attempt FC23 bulk-read optimization for S7.\n- **STOP-mode behavior non-deterministic** across firmware bands — treat both read/write STOP-mode responses as unavailable.\n\nTwo items flagged as unconfirmed rumour (V2.0+ float byte-order claim, STOP-mode caching location).\n\nNo code, no tests — implementation lands in PRs 56+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 31 citations present\n- [x] Section structure matches dl205.md template","head":"phase-3-pr54-s7-research-doc","base":"v2"}

View File

@@ -1 +0,0 @@
{"title":"Phase 3 PR 55 -- Mitsubishi MELSEC Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/mitsubishi.md` (451 lines) covering MELSEC Q/L/iQ-R/iQ-F/FX3U Modbus TCP behavior. Mirrors `docs/v2/dl205.md` template for per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **Module naming trap** — `QJ71MB91` is SERIAL RTU, not TCP. TCP module is `QJ71MT91`. Surface clearly in driver docs.\n- **No canonical mapping** — per-site 'Modbus Device Assignment Parameter' block (up to 16 entries). Treat mapping as runtime config.\n- **X/Y hex vs octal depends on family** — Q/L/iQ-R use HEX (X20 = decimal 32); FX/iQ-F use OCTAL (X20 = decimal 16). Helper must take a family selector.\n- **Word order CDAB default** across all MELSEC families (opposite of Siemens S7). Driver Mitsubishi profile default: `ByteOrder.WordSwap`.\n- **D-registers binary by default** (opposite of DL205's BCD default). Caller opts in to `Bcd16`/`Bcd32` when ladder uses BCD.\n- **FX5U needs firmware ≥ 1.060** for Modbus TCP server — older is client-only.\n- **FX3U-ENET vs FX3U-ENET-P502 vs FX3U-ENET-ADP** — only the middle one binds port 502; the last has no Modbus at all. Common operator mis-purchase.\n- **QJ71MT91 does NOT support FC22 / FC23** — iQ-R / iQ-F do. Bulk-read optimization must gate on capability.\n- **STOP-mode writes configurable** on Q/L/iQ-R/iQ-F (default accept), always rejected on FX3U-ENET.\n\nThree unconfirmed rumours flagged separately.\n\nNo code, no tests — implementation lands in PRs 58+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 17 citations present\n- [x] Per-model test naming matrix included (`Mitsubishi_QJ71MT91_*`, `Mitsubishi_FX5U_*`, `Mitsubishi_FX3U_ENET_*`, shared `Mitsubishi_Common_*`)","head":"phase-3-pr55-mitsubishi-research-doc","base":"v2"}

View File

@@ -11,9 +11,8 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
- **Core** owns the OPC UA stack, address space, session/security/subscription machinery.
- **Drivers** plug in via capability interfaces in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`, `IPerCallHostResolver`. Each driver opts into whichever it supports.
- **Server** is the OPC UA endpoint process (net10, x64). Hosts every driver except Galaxy in-process; talks to Galaxy via a named pipe because MXAccess COM is 32-bit-only.
- **Server** is the OPC UA endpoint process (net10, AnyCPU). Hosts every driver in-process. The Galaxy driver reaches MXAccess via gRPC to a separately-installed **mxaccessgw** sidecar (sibling repo); it is no longer hosted from this repo.
- **Admin** is the Blazor Server operator UI (net10, x64). Owns the Config DB draft/publish flow, ACL + role-grant authoring, fleet status + `/metrics` scrape endpoint.
- **Galaxy.Host** is a .NET Framework 4.8 x86 Windows service that wraps MXAccess COM on an STA thread for the Galaxy driver.
## Where to find what
@@ -24,11 +23,11 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
| [OpcUaServer.md](OpcUaServer.md) | Top-level server architecture — Core, driver dispatch, Config DB, generations |
| [AddressSpace.md](AddressSpace.md) | `GenericDriverNodeManager` + `ITagDiscovery` + `IAddressSpaceBuilder` |
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker``IReadable`/`IWritable` |
| [Subscriptions.md](Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount |
| [AlarmTracking.md](AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions |
| [DataTypeMapping.md](DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types |
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) |
| [AlarmTracking.md](v1/AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions (v1 archive) |
| [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
| [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` |
| [HistoricalDataAccess.md](HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability |
| [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability (v1 archive) |
| [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags |
| [ScriptedAlarms.md](ScriptedAlarms.md) | `Core.ScriptedAlarms` — script-predicate `IAlarmSource` + Part 9 state machine |
@@ -36,7 +35,7 @@ Two Core subsystems are shipped without a dedicated top-level doc; see the secti
| Project | See |
|---------|-----|
| `Core.AlarmHistorian` | [AlarmTracking.md](AlarmTracking.md) § Alarm historian sink |
| `Core.AlarmHistorian` | [AlarmTracking.md](v1/AlarmTracking.md) § Alarm historian sink (v1 archive) |
| `Analyzers` (Roslyn OTOPCUA0001) | [security.md](security.md) § OTOPCUA0001 Analyzer |
### Drivers
@@ -44,8 +43,8 @@ Two Core subsystems are shipped without a dedicated top-level doc; see the secti
| Doc | Covers |
|-----|--------|
| [drivers/README.md](drivers/README.md) | Index of the eight shipped drivers + capability matrix |
| [drivers/Galaxy.md](drivers/Galaxy.md) | Galaxy driver — MXAccess bridge, Host/Proxy split, named-pipe IPC |
| [drivers/Galaxy-Repository.md](drivers/Galaxy-Repository.md) | Galaxy-specific discovery via the ZB SQL database |
| [drivers/Galaxy.md](drivers/Galaxy.md) | Galaxy driver — in-process gRPC client to the mxaccessgw sidecar |
| [v1/drivers/Galaxy-Repository.md](v1/drivers/Galaxy-Repository.md) | Galaxy-specific discovery via the ZB SQL database (v1 archive — the gateway owns this path now) |
For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see [v2/driver-specs.md](v2/driver-specs.md).
@@ -53,10 +52,10 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
| Doc | Covers |
|-----|--------|
| [Configuration.md](Configuration.md) | appsettings bootstrap + Config DB + Admin UI draft/publish |
| [Configuration.md](v1/Configuration.md) | appsettings bootstrap + Config DB + Admin UI draft/publish (v1 archive — `OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
| [security.md](security.md) | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
| [ServiceHosting.md](ServiceHosting.md) | Three-process deploy (Server + Admin + Galaxy.Host) install/uninstall |
| [ServiceHosting.md](ServiceHosting.md) | Two-process deploy (Server + Admin) install/uninstall, plus the optional `OtOpcUaWonderwareHistorian` sidecar |
| [StatusDashboard.md](StatusDashboard.md) | Pointer — superseded by [v2/admin-ui.md](v2/admin-ui.md) |
### Client tooling
@@ -79,10 +78,10 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|-----|--------|
| [reqs/HighLevelReqs.md](reqs/HighLevelReqs.md) | HLRs — numbered system-level requirements |
| [reqs/OpcUaServerReqs.md](reqs/OpcUaServerReqs.md) | OPC UA server-layer reqs |
| [reqs/ServiceHostReqs.md](reqs/ServiceHostReqs.md) | Per-process hosting reqs |
| [v1/reqs/ServiceHostReqs.md](v1/reqs/ServiceHostReqs.md) | Per-process hosting reqs (v1 archive — only `OtOpcUa` server hosting remains in scope post-PR-7.2) |
| [reqs/ClientRequirements.md](reqs/ClientRequirements.md) | Client CLI + UI reqs |
| [reqs/GalaxyRepositoryReqs.md](reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs |
| [reqs/MxAccessClientReqs.md](reqs/MxAccessClientReqs.md) | Galaxy-scoped MXAccess reqs |
| [v1/reqs/GalaxyRepositoryReqs.md](v1/reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs (v1 archive — owned by mxaccessgw today) |
| [v1/reqs/MxAccessClientReqs.md](v1/reqs/MxAccessClientReqs.md) | Galaxy-scoped MXAccess reqs (v1 archive — owned by mxaccessgw today) |
| [reqs/StatusDashboardReqs.md](reqs/StatusDashboardReqs.md) | Pointer — superseded by Admin UI |
## Implementation history (`docs/v2/`)
@@ -96,4 +95,11 @@ Design decisions + phase plans + execution notes. Load-bearing cross-references
- [v2/driver-specs.md](v2/driver-specs.md) — per-driver addressing + quirks for every shipped protocol
- [v2/dev-environment.md](v2/dev-environment.md) — dev-box bootstrap
- [v2/test-data-sources.md](v2/test-data-sources.md) — integration-test simulator matrix (includes the pinned libplctag `ab_server` version for AB CIP tests)
- [v2/multi-host-dispatch.md](v2/multi-host-dispatch.md) — per-PLC circuit breakers (Phase 6.1 decision #144)
- [v2/v2-release-readiness.md](v2/v2-release-readiness.md) — release-readiness tracker
- [v2/lmx-followups.md](v2/lmx-followups.md) — historical Galaxy-bridge follow-ups (pre-PR-7.2)
- [v2/implementation/phase-*-*.md](v2/implementation/) — per-phase execution plans with exit-gate evidence
## v1 archive
The v1 in-process MXAccess architecture (Galaxy.Host + Galaxy.Proxy + Galaxy.Shared, .NET 4.8 x86 COM, the `OtOpcUaGalaxyHost` Windows service) was retired in PR 7.2 (2026-04-30, commit `ae7106d`). Docs that described that shape are kept under [v1/](v1/) as historical record — see [v1/README.md](v1/README.md) for the index.

View File

@@ -2,7 +2,6 @@
Getting-started guide for the FANUC FOCAS2 driver. This is the short path — for
the exhaustive per-node mapping read [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md),
for deployment details read [`docs/v2/focas-deployment.md`](../v2/focas-deployment.md),
for the test-harness map read [FOCAS-Test-Fixture.md](FOCAS-Test-Fixture.md).
## What it talks to
@@ -210,8 +209,9 @@ latency spike once per cadence.
driver surface via `FakeFocasClient`. Includes the alarm-projection raise /
clear diffing tests.
- **Integration tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
hold the Docker simulator scaffold (Stream B / C of the simulator plan —
`docs/v2/implementation/focas-simulator-plan.md`).
hold the Docker simulator scaffold; see
[`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md)
for what the simulator emits vs. real CNC behaviour.
- **E2E script** — `scripts/e2e/test-focas.ps1` stages Host + Proxy + a real
CNC (or the simulator) and exercises connect → read → write → subscribe
round-trips. See [`docs/drivers/FOCAS-Test-Fixture.md`](FOCAS-Test-Fixture.md)

View File

@@ -1,211 +1,92 @@
# Galaxy Driver
The Galaxy driver bridges OtOpcUa to AVEVA System Platform (Wonderware) Galaxies through the `ArchestrA.MxAccess` COM API plus the Galaxy Repository SQL database. It is one driver of seven in the OtOpcUa platform (see [drivers/README.md](README.md) for the full list); all other drivers run in-process in the main Server (.NET 10 x64). Galaxy is the exception — it runs as its own Windows service and talks to the Server over a local named pipe.
The Galaxy driver bridges OtOpcUa to AVEVA System Platform (Wonderware) Galaxies. It is a **Tier-A in-process driver** that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed `mxaccessgw` server (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`). The gateway owns the MXAccess COM apartment, the STA + Win32 message pump, the Galaxy Repository SQL reader, and the Historian SDK — all the bits that need x86 / .NET Framework 4.8 / COM interop. The driver itself is platform-agnostic and contains no COM, no STA thread, and no x86 bitness constraint.
For the decision record on why Galaxy is out-of-process and how the refactor was staged, see [docs/v2/plan.md §4 Galaxy/MXAccess as Out-of-Process Driver](../v2/plan.md). For the full driver spec (addressing, data-type map, config shape), see [docs/v2/driver-specs.md §1](../v2/driver-specs.md).
For the driver spec (capability surface, config shape, addressing), see [docs/v2/driver-specs.md §1](../v2/driver-specs.md). For the gateway setup recipe, see [docs/v2/Galaxy.ParityRig.md](../v2/Galaxy.ParityRig.md). For tracing, metrics, and soak profile, see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).
## Project Split
> **Note**: the related drivers `Galaxy-Repository.md` and `Galaxy-Test-Fixture.md` describe the previous v1 / out-of-process topology and are being moved to `docs/v1/` by a parallel cleanup track. Use `Galaxy.ParityRig.md` and the `mxaccessgw` repo for current testing.
Galaxy ships as three projects:
## Architecture
| Project | Target | Role |
|---------|--------|------|
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/` | .NET Standard 2.0 | IPC contracts (MessagePack records + `MessageKind` enum) referenced by both sides |
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/` | .NET Framework 4.8 **x86** | Separate Windows service hosting the MXAccess COM objects, STA thread + Win32 message pump, Galaxy Repository reader, Historian SDK, runtime-probe manager |
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/` | .NET 10 (matches Server) | `GalaxyProxyDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IRediscoverable, IHostConnectivityProbe` — loaded in-process by the Server; every call forwards over the pipe to the Host |
The Shared assembly is the **only** contract between the two runtimes. It carries no COM or SDK references so Proxy (net10) can reference it without dragging x86 code into the Server process.
## Why Out-of-Process
Two reasons drive the split, per `docs/v2/plan.md`:
1. **Bitness constraint.** MXAccess is 32-bit COM only — `ArchestrA.MxAccess.dll` in `Program Files (x86)\ArchestrA\Framework\bin` has no 64-bit variant. The main OtOpcUa Server is .NET 10 x64 (the OPC Foundation stack, SqlClient, and every other non-Galaxy driver target 64-bit). In-process hosting would force the whole Server to x86, which every other driver project would then inherit.
2. **Tier-C stability isolation.** Galaxy is classified Tier C in [docs/v2/driver-stability.md](../v2/driver-stability.md) — the COM runtime, STA thread, Aveva Historian SDK, and SQL queries all have crash/hang modes that can take down the hosting process. Isolating the driver in its own Windows service means a COM deadlock, AccessViolation in an unmanaged Historian DLL, or a runaway SQL query never takes the Server endpoint down. The Proxy-side supervisor restarts the Host with crash-loop circuit-breaker.
The same Tier-C isolation story applies to FOCAS (decision record in `docs/v2/plan.md` §7), which is the second out-of-process driver.
## IPC Transport
`GalaxyProxyDriver``GalaxyIpcClient` → named pipe → `Galaxy.Host` pipe server.
- Pipe name: `otopcua-galaxy-{DriverInstanceId}` (localhost-only, no TCP surface)
- Wire format: MessagePack-CSharp, length-prefixed frames
- ACL: pipe is created with a DACL that grants `ReadWrite | Synchronize` only to the configured Server service-principal SID + denies `LocalSystem`. The per-connection SID check in `PipeServer.VerifyCaller` is the real authorization boundary — any caller whose impersonated token SID doesn't match the allowed SID is dropped before the first frame is read.
- Handshake: Proxy presents a shared secret at `OpenSessionRequest`; Host rejects anything else with `MessageKind.OpenSessionResponse{Success=false}`
- Heartbeat: Proxy sends a periodic ping; missed heartbeats trigger the Proxy-side crash-loop supervisor to restart the Host
Every capability call on `GalaxyProxyDriver` (Read, Write, Subscribe, HistoryRead*, etc.) serializes a `*Request`, awaits the matching `*Response` via a `CallAsync<TReq, TResp>` helper, and rehydrates the result into the `Core.Abstractions` shape the Server expects.
## STA Thread Requirement (Host-side)
MXAccess COM objects — `LMXProxyServer` instantiation, `Register`, `AddItem`, `AdviseSupervisory`, `Write`, and cleanup calls — must all execute on the same Single-Threaded Apartment. Calling a COM object from the wrong thread causes marshalling failures or silent data corruption.
`StaComThread` in the Host provides that thread with the apartment state set before the thread starts:
```csharp
_thread = new Thread(ThreadEntry) { Name = "MxAccess-STA", IsBackground = true };
_thread.SetApartmentState(ApartmentState.STA);
```
+---------------------------------------+
| OtOpcUa.Server (.NET 10 AnyCPU) |
| GalaxyDriver (in-process) |
| ITagDiscovery / IReadable / |
| IWritable / ISubscribable / |
| IRediscoverable / |
| IHostConnectivityProbe |
+-------------------+-------------------+
|
gRPC (default http://localhost:5120)
|
v
+---------------------------------------+
| mxaccessgw (sibling repo) |
| +-------------------------------+ |
| | MxGateway.Worker (x86 net48) | |
| | STA + WM_APP pump | |
| | ArchestrA.MxAccess COM | |
| | Galaxy Repository SQL | |
| | Wonderware Historian SDK | |
| +-------------------------------+ |
+---------------------------------------+
```
Work items queue via `RunAsync(Action)` or `RunAsync<T>(Func<T>)` into a `ConcurrentQueue<Action>` and post `WM_APP` to wake the pump. Each work item is wrapped in a `TaskCompletionSource` so callers can `await` the result from any thread — including the IPC handler thread that receives the inbound pipe request.
History reads + alarm-condition tracking moved server-side in PR 7.2 (`IHistoryRouter`, `AlarmConditionService`). Galaxy no longer implements `IHistoryProvider` or `IAlarmSource` of its own.
## Win32 Message Pump (Host-side)
## Project Layout
COM callbacks (`OnDataChange`, `OnWriteComplete`) are delivered through the Windows message loop. `StaComThread` runs a standard Win32 message pump via P/Invoke:
The driver ships as a single project: `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/` (.NET 10, AnyCPU). Sub-folders:
1. `PeekMessage` primes the message queue (required before `PostThreadMessage` works)
2. `GetMessage` blocks until a message arrives
3. `WM_APP` drains the work queue
4. `WM_APP + 1` drains the queue and posts `WM_QUIT` to exit the loop
5. All other messages go through `TranslateMessage` / `DispatchMessage` for COM callback delivery
| Folder | Role |
|--------|------|
| `Browse/` | Static-side discovery: `GalaxyDiscoverer` walks the gateway's hierarchy + attribute-set RPCs, `DataTypeMap` and `SecurityMap` translate Galaxy types and security classifications into OPC UA equivalents, `AlarmRefBuilder` extracts alarm-bearing attribute references for the server-layer alarm engine. `IGalaxyHierarchySource` + `GatewayGalaxyHierarchySource` + `TracedGalaxyHierarchySource` decorate the gateway browse RPC; `IGalaxyDeployWatchSource` + `GatewayGalaxyDeployWatchSource` + `DeployWatcher` drive `IRediscoverable`. |
| `Runtime/` | Live data path: `EventPump` runs the gateway's `StreamEvents` RPC and fans out to subscribers via a bounded channel; `GalaxyMxSession` is the read-side handle; `GatewayGalaxySubscriber` + `GatewayGalaxyDataWriter` (each with a `Traced*` decorator) implement `ISubscribable` / `IWritable`; `SubscriptionRegistry` tracks subscription state for replay; `ReconnectSupervisor` owns the backoff loop and triggers `ReplaySubscriptions` on session loss; `StatusCodeMap` translates gateway StatusCodes to OPC UA; `MxValueDecoder` / `MxValueEncoder` handle scalar + array marshalling; `GalaxyTelemetry` + `GalaxySubscriptionHandle` round out the surface. |
| `Health/` | `HostStatusAggregator` rolls per-platform probe state into the driver's `IHostConnectivityProbe` view; `PerPlatformProbeWatcher` listens on the gateway's per-host status stream; `HostConnectivityForwarder` pushes transitions out to the server's connectivity bus. |
| `Config/` | `GalaxyDriverOptions` and the four nested option records (`GalaxyGatewayOptions`, `GalaxyMxAccessOptions`, `GalaxyRepositoryOptions`, `GalaxyReconnectOptions`). |
Without this pump MXAccess callbacks never fire and the driver delivers no live data.
Project root files:
## LMXProxyServer COM Object
- `GalaxyDriver.cs``IDriver` + capability-interface implementation; composes the Browse / Runtime / Health collaborators.
- `GalaxyDriverFactoryExtensions.cs` — DI registration helper used by the server's driver bootstrap.
`MxProxyAdapter` wraps the real `ArchestrA.MxAccess.LMXProxyServer` COM object behind the `IMxProxy` interface so Host unit tests can substitute a fake proxy without requiring the ArchestrA runtime. Lifecycle:
## Capability Surface
1. **`Register(clientName)`** — Creates a new `LMXProxyServer` instance, wires up `OnDataChange` and `OnWriteComplete` event handlers, calls `Register` to obtain a connection handle
2. **`Unregister(handle)`** — Unwires event handlers, calls `Unregister`, releases the COM object via `Marshal.ReleaseComObject`
`GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IRediscoverable, IHostConnectivityProbe, IDisposable`.
## Register / AddItem / AdviseSupervisory Pattern
| Capability | Implementation entry point |
|------------|---------------------------|
| `ITagDiscovery` | `Browse/GalaxyDiscoverer.cs` |
| `IRediscoverable` | `Browse/DeployWatcher.cs` |
| `IReadable` | `Runtime/GalaxyMxSession.cs` |
| `IWritable` | `Runtime/GatewayGalaxyDataWriter.cs` |
| `ISubscribable` | `Runtime/GatewayGalaxySubscriber.cs` (driven by `EventPump`) |
| `IHostConnectivityProbe` | `Health/HostStatusAggregator.cs` |
Every MXAccess data operation follows a three-step pattern, all executed on the STA thread:
## Configuration
1. **`AddItem(handle, address)`** — Resolves a Galaxy tag reference (e.g., `TestMachine_001.MachineID`) to an integer item handle
2. **`AdviseSupervisory(handle, itemHandle)`** — Subscribes the item for supervisory data-change callbacks
3. The runtime begins delivering `OnDataChange` events
`DriverConfig` JSON binds to `Config/GalaxyDriverOptions.cs`. The four sections are:
For writes, after `AddItem` + `AdviseSupervisory`, `Write(handle, itemHandle, value, securityClassification)` sends the value; `OnWriteComplete` confirms or rejects. Cleanup reverses: `UnAdviseSupervisory` then `RemoveItem`.
- **`Gateway`** — endpoint, API key secret ref, TLS knobs, connect/call/stream timeouts. `StreamTimeoutSeconds = 0` keeps the long-lived `StreamEvents` RPC open for the driver's lifetime.
- **`MxAccess`** — `ClientName` (must be unique per OtOpcUa instance — redundancy pairs enforce uniqueness at install time), `PublishingIntervalMs` (forwarded as `buffered_update_interval_ms` on subscribe), `WriteUserId` for ArchestrA secured-write, `EventPumpChannelCapacity` (default 50_000 — one second of headroom at 50k tags / 1Hz; tune via the `galaxy.events.dropped` metric).
- **`Repository`** — `DiscoverPageSize`, `WatchDeployEvents`.
- **`Reconnect`** — `InitialBackoffMs`, `MaxBackoffMs`, `ReplayOnSessionLost` (calls the gateway's `ReplaySubscriptions` RPC after reconnect rather than re-issuing subscribe-bulk for every tag).
## OnDataChange and OnWriteComplete Callbacks
Full per-field descriptions live in `Config/GalaxyDriverOptions.cs`. The full JSON skeleton is reproduced in [docs/v2/driver-specs.md §1](../v2/driver-specs.md).
### OnDataChange
## Reconnect + Replay
Fired by the COM runtime on the STA thread when a subscribed tag changes. The handler in `MxAccessClient.EventHandlers.cs`:
`ReconnectSupervisor` owns an exponential-backoff loop bounded by `Reconnect.InitialBackoffMs` / `MaxBackoffMs`. On session loss it tears down the gRPC channel, redials, and — when `ReplayOnSessionLost = true` — calls the gateway's `ReplaySubscriptions` RPC with the cached subscription set from `SubscriptionRegistry` instead of re-subscribing tag-by-tag. The gateway's worker then re-issues `AdviseSupervisory` server-side under the apartment lock.
1. Maps the integer `phItemHandle` back to a tag address via `_handleToAddress`
2. Maps the MXAccess quality code to the internal `Quality` enum
3. Checks `MXSTATUS_PROXY` for error details and adjusts quality
4. Converts the timestamp to UTC
5. Constructs a `Vtq` (Value/Timestamp/Quality) and delivers it to:
- The stored per-tag subscription callback
- Any pending one-shot read completions
- The global `OnTagValueChanged` event (consumed by the Host's subscription dispatcher, which packages changes into `DataChangeEventArgs` and forwards them over the pipe to `GalaxyProxyDriver.OnDataChange`)
## Testing
### OnWriteComplete
- **Unit tests**: `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/` — fakes the gateway gRPC surface; covers Browse, Runtime, Health, and Config in isolation.
- **Parity rig + dev-rig walkthrough**: see [docs/v2/Galaxy.ParityRig.md](../v2/Galaxy.ParityRig.md). The rig stands up a real `mxaccessgw` against a live Galaxy and exercises the full read / write / subscribe / rediscover path.
- **Performance + soak**: see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).
Fired when the runtime acknowledges or rejects a write. The handler resolves the pending `TaskCompletionSource<bool>` for the item handle. If `MXSTATUS_PROXY.success == 0` the write is considered failed and the error detail is logged.
## Operational Notes
## Reconnection Logic
`MxAccessClient` implements automatic reconnection through two mechanisms.
### Monitor loop
`StartMonitor` launches a background task that polls at `MonitorIntervalSeconds`. On each cycle:
- If the state is `Disconnected` or `Error` and `AutoReconnect` is enabled, it calls `ReconnectAsync`
- If connected and a probe tag is configured, it checks the probe staleness threshold
### Reconnect sequence
`ReconnectAsync` performs a full disconnect-then-connect cycle:
1. Increment the reconnect counter
2. `DisconnectAsync` — tear down all active subscriptions (`UnAdviseSupervisory` + `RemoveItem` for each), detach COM event handlers, call `Unregister`, clear all handle mappings
3. `ConnectAsync` — create a fresh `LMXProxyServer`, register, replay all stored subscriptions, re-subscribe the probe tag
Stored subscriptions (`_storedSubscriptions`) persist across reconnects. `ReplayStoredSubscriptionsAsync` iterates the stored entries and calls `AddItem` + `AdviseSupervisory` for each.
## Probe Tag Health Monitoring
A configurable probe tag (e.g., a frequently updating Galaxy attribute) serves as a connection health indicator. After connecting, the client subscribes to the probe tag and records `_lastProbeValueTime` on every `OnDataChange`. The monitor loop compares `DateTime.UtcNow - _lastProbeValueTime` against `ProbeStaleThresholdSeconds`; if the probe has not updated within the window, the connection is assumed stale and a reconnect is forced. This catches scenarios where the COM connection is technically alive but the runtime has stopped delivering data.
## Per-Host Runtime Status Probes (`<Host>.ScanState`)
Separate from the connection-level probe, the driver advises `<HostName>.ScanState` on every deployed `$WinPlatform` and `$AppEngine` in the Galaxy. These probes track per-host runtime state so the Admin UI dashboard can report "this specific Platform / AppEngine is off scan" and the driver can proactively invalidate every OPC UA variable hosted by the stopped object — preventing MXAccess from serving stale Good-quality cached values to clients who read those tags while the host is down.
Enabled by default via `MxAccess.RuntimeStatusProbesEnabled`; see [Configuration](../Configuration.md#mxaccess) for the two config fields.
### How it works
`GalaxyRuntimeProbeManager` lives in `Driver.Galaxy.Host` alongside the rest of the MXAccess code. It is owned by the Host's subscription dispatcher and runs a three-state machine per host (Unknown / Running / Stopped):
1. **Discovery** — After the Host completes `BuildAddressSpace`, the manager filters the hierarchy to rows where `CategoryId == 1` (`$WinPlatform`) or `CategoryId == 3` (`$AppEngine`) and issues `AdviseSupervisory` for `<TagName>.ScanState` on each one. Probes are driver-owned, not ref-counted against client subscriptions, and persist across address-space rebuilds via a `Sync` diff.
2. **Transition predicate** — A probe callback is interpreted as `isRunning = vtq.Quality.IsGood() && vtq.Value is bool b && b`. Everything else (explicit `ScanState = false`, bad quality, communication errors) means **Stopped**.
3. **On-change-only delivery**`ScanState` is delivered only when the value actually changes. A stably Running host may go hours without a callback. `Tick()` does NOT run a starvation check on Running entries — the only time-based transition is **Unknown → Stopped** when the initial callback hasn't arrived within `RuntimeStatusUnknownTimeoutSeconds` (default 15s). This protects against a probe that fails to resolve at all without incorrectly flipping healthy long-running hosts.
4. **Transport gating** — When `IMxAccessClient.State != Connected`, `GetSnapshot()` forces every entry to `Unknown`. The dashboard shows the Connection panel as the primary signal in that case rather than misleading operators with "every host stopped".
5. **Subscribe failure rollback** — If `SubscribeAsync` throws for a new probe (SDK failure, broker rejection, transport error), the manager rolls back both `_byProbe` and `_probeByGobjectId` so the probe never appears in `GetSnapshot()`. Stability review 2026-04-13 Finding 1.
### Subtree quality invalidation on transition
When a host transitions **Running → Stopped**, the probe manager invokes a callback that walks `_hostedVariables[gobjectId]` — the set of every OPC UA variable transitively hosted by that Galaxy object — and sets each variable's `StatusCode` to `BadOutOfService`. **Stopped → Running** calls `ClearHostVariablesBadQuality` to reset each to `Good` so the next on-change MXAccess update repopulates the value.
The hosted-variables map is built once per `BuildAddressSpace` by walking each object's `HostedByGobjectId` chain up to the nearest Platform or Engine ancestor. A variable hosted by an Engine inside a Platform lands in both the Engine's list and the Platform's list, so stopping the Platform transitively invalidates every descendant Engine's variables.
### Read-path short-circuit (`IsTagUnderStoppedHost`)
The Host's Read handler checks `IsTagUnderStoppedHost(tagRef)` (a reverse-index lookup `_hostIdsByTagRef[tagRef]``GalaxyRuntimeProbeManager.IsHostStopped(hostId)`) before the MXAccess round-trip. When the owning host is Stopped, the handler returns a synthesized `DataValue { Value = cachedVar.Value, StatusCode = BadOutOfService }` directly without touching MXAccess. This guarantees clients see a uniform `BadOutOfService` on every descendant tag of a stopped host, regardless of whether they're reading or subscribing.
### Deferred dispatch — the STA deadlock
**Critical**: probe transition callbacks must **not** run synchronously on the STA thread that delivered the `OnDataChange`. `MarkHostVariablesBadQuality` takes the subscription dispatcher lock, which may be held by a worker thread currently inside `Read` waiting on an `_mxAccessClient.ReadAsync()` round-trip that is itself waiting for the STA thread. Classic circular wait — the first real deploy of this feature hung inside 30 seconds from exactly this pattern.
The fix is a deferred-dispatch queue: probe callbacks enqueue the transition onto `ConcurrentQueue<(int GobjectId, bool Stopped)>` and set the existing dispatch signal. The dispatch thread drains the queue inside its existing 100ms `WaitOne` loop — outside any locks held by the STA path — and then calls `MarkHostVariablesBadQuality` / `ClearHostVariablesBadQuality` under its own natural lock acquisition. No circular wait, no STA involvement.
### Dashboard and health surface
- Admin UI **Galaxy Runtime** panel shows per-host state with Name / Kind / State / Since / Last Error columns. Panel color is green (all Running), yellow (any Unknown, none Stopped), red (any Stopped), gray (MXAccess transport disconnected)
- `HealthCheckService.CheckHealth` rolls overall driver health to `Degraded` when any host is Stopped
See [Status Dashboard](../StatusDashboard.md#galaxy-runtime) for the field table and [Configuration](../Configuration.md#mxaccess) for the config fields.
## Request Timeout Safety Backstop
Every sync-over-async site on the OPC UA stack thread that calls into Galaxy (`Read`, `Write`, address-space rebuild probe sync) is wrapped in a bounded `SyncOverAsync.WaitSync(...)` helper with timeout `MxAccess.RequestTimeoutSeconds` (default 30s). Inner `ReadTimeoutSeconds` / `WriteTimeoutSeconds` bounds on the async path are the first line of defense; the outer wrapper is a backstop so a scheduler stall, slow reconnect, or any other non-returning async path cannot park the stack thread indefinitely.
On timeout, the underlying task is **not** cancelled — it runs to completion on the thread pool and is abandoned. This is acceptable because Galaxy IPC clients are shared singletons and the abandoned continuation does not capture request-scoped state. The OPC UA stack receives `StatusCodes.BadTimeout` on the affected operation.
`ConfigurationValidator` enforces `RequestTimeoutSeconds >= 1` and warns when it is set below the inner Read/Write timeouts (operator misconfiguration). Stability review 2026-04-13 Finding 3.
All capability calls at the Server dispatch layer are additionally wrapped by `CapabilityInvoker` (Core/Resilience/) which runs them through a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. `OTOPCUA0001` analyzer enforces the wrap at build time.
## Why Marshal.ReleaseComObject Is Needed
The .NET Framework runtime's garbage collector releases COM references non-deterministically. For MXAccess, delayed release can leave stale COM connections open, preventing clean re-registration. `MxProxyAdapter.Unregister` calls `Marshal.ReleaseComObject(_lmxProxy)` in a `finally` block to immediately drive the COM reference count to zero. This ensures the underlying COM server is freed before a reconnect attempt creates a new instance.
## Tag Discovery and Historical Data
Tag discovery (the Galaxy Repository SQL reader + `LocalPlatform` scope filter) is covered in [Galaxy-Repository.md](Galaxy-Repository.md). The Galaxy driver is `ITagDiscovery` for the Server's bootstrap path and `IRediscoverable` for the on-change-redeploy path.
Historical data access (raw, processed, at-time, events) runs against the Aveva Historian via the `aahClientManaged` SDK and is exposed through the Galaxy driver's `IHistoryProvider` implementation. See [HistoricalDataAccess.md](../HistoricalDataAccess.md).
## Key source files
Host-side (`.NET 4.8 x86`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/`):
- `Backend/MxAccess/StaComThread.cs` — STA thread and Win32 message pump
- `Backend/MxAccess/MxAccessClient.cs` — Core client (partial)
- `Backend/MxAccess/MxAccessClient.Connection.cs` — Connect / disconnect / reconnect
- `Backend/MxAccess/MxAccessClient.Subscription.cs` — Subscribe / unsubscribe / replay
- `Backend/MxAccess/MxAccessClient.ReadWrite.cs` — Read and write operations
- `Backend/MxAccess/MxAccessClient.EventHandlers.cs``OnDataChange` / `OnWriteComplete` handlers
- `Backend/MxAccess/MxAccessClient.Monitor.cs` — Background health monitor
- `Backend/MxAccess/MxProxyAdapter.cs` — COM object wrapper
- `Backend/MxAccess/GalaxyRuntimeProbeManager.cs` — Per-host `ScanState` probes, state machine, `IsHostStopped` lookup
- `Backend/Historian/HistorianDataSource.cs``aahClientManaged` SDK wrapper (see [HistoricalDataAccess.md](../HistoricalDataAccess.md))
- `Ipc/GalaxyIpcServer.cs` — Named-pipe server, message dispatch
- `Domain/IMxAccessClient.cs` — Client interface
Shared (`.NET Standard 2.0`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/`):
- `Contracts/MessageKind.cs` — IPC message kinds (`ReadRequest`, `HistoryReadRequest`, `OpenSessionResponse`, …)
- `Contracts/*.cs` — MessagePack DTOs for every request/response pair
Proxy-side (`.NET 10`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/`):
- `GalaxyProxyDriver.cs``IDriver`/`ITagDiscovery`/`IReadable`/`IWritable`/`ISubscribable`/`IAlarmSource`/`IHistoryProvider`/`IRediscoverable`/`IHostConnectivityProbe` implementation; every method forwards via `GalaxyIpcClient`
- `Ipc/GalaxyIpcClient.cs` — Named-pipe client, `CallAsync<TReq, TResp>`, reconnect on broken pipe
- `GalaxyProxySupervisor.cs` — Host-process monitor, crash-loop circuit-breaker, Host relaunch
- **MXAccess `ClientName` collisions**: two OtOpcUa instances sharing a `ClientName` cause the older Wonderware session to lose subscription state. Redundancy pairs (decision #149) enforce uniqueness via install scripts.
- **Channel saturation**: `galaxy.events.dropped > 0` indicates `EventPump` is back-pressured. Raise `EventPumpChannelCapacity` or investigate downstream slowness in the server-side fan-out.
- **Connectivity surface**: per-platform probe state is exposed through `IHostConnectivityProbe` and aggregated by the server's connectivity bus — there is no driver-private dashboard surface anymore. The Admin UI's Host Status panel is the consumer.

File diff suppressed because it is too large Load Diff

29
docs/v1/README.md Normal file
View File

@@ -0,0 +1,29 @@
# v1 documentation archive
This folder contains documentation that described the original v1
in-process MXAccess architecture (`Galaxy.Host` + `Galaxy.Proxy` +
`Galaxy.Shared` three-project split, .NET 4.8 x86 + COM apartment, the
`OtOpcUaGalaxyHost` Windows service). That architecture was retired in
PR 7.2 (merged 2026-04-30 at commit `ae7106d`). These docs are kept as
the historical record of how the system worked before the v2-mxgw
migration; treat their content as accurate at the time of writing, NOT
as current state.
For current architecture see:
- `CLAUDE.md` — agent-facing v2 overview
- `docs/drivers/Galaxy.md` — current Galaxy driver doc
- `docs/v2/Galaxy.ParityRig.md` — current testing setup
- `docs/v2/Galaxy.Performance.md` — observability + perf
| File | What it covered |
|---|---|
| `AlarmTracking.md` | v1 alarm-tracking flow through the in-process MXAccess client |
| `Configuration.md` | v1 server configuration (`OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
| `DataTypeMapping.md` | Galaxy `mx_data_type` → OPC UA type mapping (still accurate as a reference; the live mapping logic is in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
| `HistoricalDataAccess.md` | v1 IHistoryProvider on the Host side; current path is the server-level HistoryRouter + Wonderware sidecar |
| `Subscriptions.md` | v1 MXAccess subscription mechanics; current path uses gateway StreamEvents |
| `drivers/Galaxy-Repository.md` | v1 Host-side ZB SQL repository client; the gateway owns this path now |
| `drivers/Galaxy-Test-Fixture.md` | v1 test-fixture setup (parity tests + Galaxy.Host EXE spawn) |
| `reqs/GalaxyRepositoryReqs.md`, `reqs/MxAccessClientReqs.md` | Original Phase 0 requirements; satisfied in mxaccessgw repo today |
| `reqs/ServiceHostReqs.md` | Service-hosting requirements including `OtOpcUaGalaxyHost` (GHX-* section); only `OtOpcUa` server hosting remains in scope post-7.2 |

View File

@@ -1,3 +1,15 @@
> **✅ Completed 2026-04-30 — historical record of the parity-rig validation gate for PR 7.2.**
>
> The matrix below was the go/no-go gate for retiring the legacy
> Galaxy.Host backend (PR 7.2). Final run on the dev rig 2026-04-30
> returned 14 passed / 1 skipped / 0 failed; PR 7.2 (commit `fe91d42`)
> deleted the legacy projects + service the next day. The "Running
> the matrix" section is preserved for historical reproducibility but
> the test projects it references (`Driver.Galaxy.ParityTests`) were
> deleted alongside the legacy backend; this matrix is no longer
> runnable. Current Galaxy testing flows through the gateway's own
> test suite (sibling mxaccessgw repo).
# Galaxy backend parity matrix
This document tracks the scenario × result matrix that the

View File

@@ -1,15 +1,30 @@
# Galaxy parity rig — runbook
> ✅ **Completed 2026-04-30 — historical record.** This runbook is the
> recipe that produced the green parity matrix that gated PR 7.2
> (retire legacy Galaxy projects, merged at commit `ae7106d`). The
> matrix it produced is captured in
> [`Galaxy.ParityMatrix.md`](Galaxy.ParityMatrix.md), also marked
> historical. The test project this doc drove
> (`Driver.Galaxy.ParityTests`) was deleted in PR 7.2, along with
> `Driver.Galaxy.{Host,Proxy,Shared}` and the `OtOpcUaGalaxyHost`
> Windows service. **You cannot re-run this rig today.** Current
> Galaxy testing flows through the gateway's own test suite in the
> sibling `mxaccessgw` repo.
>
> The text below is preserved as-written so the migration trail (what
> was tested, against what shape, with what env vars) stays auditable.
Brings up both Galaxy backends side-by-side against a single live Galaxy
so the parity matrix in `docs/v2/Galaxy.ParityMatrix.md` and the soak
scenario in `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/SoakScenarioTests.cs`
can run for real. Closing the parity matrix is the gate for PR 7.2
can run for real. Closing the parity matrix was the gate for PR 7.2
(retire legacy Galaxy projects).
## Conceptual layout
```
Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86)
Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86) [DELETED in PR 7.2]
│ └── MxAccess COM, ClientName "OtOpcUa-Galaxy.Host"
│ └── named pipe "OtOpcUaGalaxy"
│ ▲
@@ -29,17 +44,19 @@ Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86)
Both halves talk to the **same Galaxy** through **two distinct MxAccess
sessions** (different ClientNames so they don't evict each other).
## What's already on this dev box
## What was on the dev box at the time
Per `~/.claude/projects/.../memory/`:
Per `~/.claude/projects/.../memory/` *as of the rig run*:
- **AVEVA System Platform + Galaxy + MXAccess runtime** — `project_aveva_platform_installed.md`.
- **`OtOpcUaGalaxyHost`** Windows service running as `dohertj2`, NSSM-wrapped,
binary at `C:\publish\OtOpcUaGalaxyHost\OtOpcUa.Driver.Galaxy.Host.exe`,
shared secret at `.local/galaxy-host-secret.txt`, ZB SQL on `localhost:1433`
`project_galaxy_host_installed.md`.
- **Parity test project** (`Driver.Galaxy.ParityTests`) committed and
skip-clean — runs as soon as the mxgw half resolves.
`project_galaxy_host_installed.md`. **(Service uninstalled and binary
retired as part of PR 7.2; the host source project no longer exists in
this repo.)**
- **Parity test project** (`Driver.Galaxy.ParityTests`) — committed and
skip-clean at the time of the rig run. **Deleted in PR 7.2.**
## Setup steps (one-time)
@@ -282,7 +299,7 @@ sees the change:
```powershell
graccess object deploy --galaxy ZB --name OtOpcUaParityTest_001 `
--confirm --confirm-target OtOpcUaParityTest_001
sc.exe restart OtOpcUaGalaxyHost
sc.exe restart OtOpcUaGalaxyHost # service no longer exists post-PR-7.2; in the modern shape, restart mxaccessgw instead
```
Then re-run the parity matrix. The previously-skipped scenarios should
@@ -343,11 +360,14 @@ Galaxy with a script that imports 50k attributes onto a generated UDO
- **`LegacySkipReason` says "Galaxy ZB SQL not reachable on
localhost:1433"** — SQL Server isn't running, or its TCP listener is
off. Check `services.msc` for the SQL Server (default) instance.
- **`LegacySkipReason` says "Galaxy.Host EXE not built"** — the parity
harness looks under `src/.../bin/Debug/net48/`. Build it once:
`dotnet build src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`. Note the
separately-published copy at `C:\publish\OtOpcUaGalaxyHost\` is for
the Windows service; the parity harness spawns its own subprocess.
- **`LegacySkipReason` says "Galaxy.Host EXE not built"** — at rig time
the parity harness looked under
`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/` for the
EXE it spawned as a subprocess, separate from the published copy at
`C:\publish\OtOpcUaGalaxyHost\` used by the Windows service. **Both
the source project and the published binary were removed in PR 7.2,
so this troubleshooting branch no longer applies — the legacy half
cannot be brought up at all.**
- **Both halves resolve but parity scenarios assert deltas** — that's
the expected outcome the rig exists to surface. Review each delta
against `docs/v2/Galaxy.ParityMatrix.md`'s "Accepted deltas" section

View File

@@ -10,289 +10,65 @@
### Summary
Out-of-process **Tier C** driver bridging AVEVA System Platform (Wonderware) Galaxies. The existing v1 implementation is refactored behind the new driver capability interfaces and hosted in a separate Windows service (.NET 4.8 x86) that communicates with the main OtOpcUa server (.NET 10 x64) via named pipes + MessagePack. Hosted out-of-process for **two reasons**: COM/.NET 4.8 x86 bitness constraint **and** Tier C stability isolation (per `driver-stability.md`). FOCAS is the second Tier C driver, also out-of-process — see §7.
### Library & Dependencies
| Component | Package / Source | Version | Target | Notes |
|-----------|------------------|---------|--------|-------|
| **MXAccess COM** | `ArchestrA.MxAccess` (GAC / `lib/ArchestrA.MxAccess.dll`) | version-neutral late-bound | .NET 4.8 x86 | Pinned via `<Reference Include="ArchestrA.MxAccess">` with `EmbedInteropTypes=false`; interfaces: `LMXProxyServer`, `ILMXProxyServerEvents`, `MXSTATUS_PROXY` |
| **Galaxy DB client** | `System.Data.SqlClient` (BCL) | BCL | .NET 4.8 x86 | Direct SQL for hierarchy/attribute/change-detection queries |
| **Wonderware Historian SDK** | `aahClientManaged`, `aahClientCommon` | Historian-shipped | .NET 4.8 x86 | Optional — loaded only when `Historian.Enabled=true` |
| **MessagePack-CSharp** | `MessagePack` NuGet | 2.x | .NET Standard 2.0 (Shared) | IPC serialization; shared contract between Proxy and Host |
| **Named pipes** | `System.IO.Pipes` (BCL) | BCL | both sides | IPC transport, localhost only |
### Required Components
- **AVEVA System Platform / ArchestrA Platform** deployed on the same machine as `Galaxy.Host` (installs MXAccess COM objects into the GAC)
- A **deployed Galaxy** with at least one $WinPlatform object hosting $AppEngine(s) hosting AutomationObjects
- **SQL Server** reachable from `Galaxy.Host` with the Galaxy repository database (default `ZB`); Windows Auth by default
- **32-bit .NET Framework 4.8** runtime on the Host machine (MXAccess is 32-bit COM, no 64-bit variant)
- **STA thread + Win32 message pump** inside the Host process for all COM calls and event callbacks (see §13)
- **Wonderware Historian** installed on-box or reachable via aah SDK — *only* if HDA is enabled
- **No external firewall ports** — MXAccess is local-machine COM/IPC; pipe is localhost-only. Galaxy DB port (default SQL 1433) if the ZB database is remote.
### Connection Settings (per driver instance, from central config DB)
All settings live under a schemaless `DriverConfig` JSON blob on the `DriverInstance` row. Current v1 equivalents (defaults and source file references in parentheses):
**MXAccess** (`MxAccessConfiguration.cs`):
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `ClientName` | string | `"LmxOpcUa"` | Registration name passed to `LMXProxyServer.Register()` |
| `NodeName` | string? | `null` | Optional ArchestrA node override (null = local) |
| `GalaxyName` | string? | `null` | Optional Galaxy name override |
| `ReadTimeoutSeconds` | int | `5` | Per-read timeout |
| `WriteTimeoutSeconds` | int | `5` | Per-write timeout |
| `RequestTimeoutSeconds` | int | `30` | Outer safety timeout around any MXAccess request |
| `MaxConcurrentOperations` | int | `10` | Pool bound on in-flight MXAccess work items |
| `MonitorIntervalSeconds` | int | `5` | Connectivity heartbeat probe interval |
| `AutoReconnect` | bool | `true` | Replay stored subscriptions on COM reconnect |
| `ProbeTag` | string? | `null` | Optional heartbeat tag for health monitoring |
| `ProbeStaleThresholdSeconds` | int | `60` | Mark connection stale if no probe callback within |
| `RuntimeStatusProbesEnabled` | bool | `true` | Auto-subscribe `ScanState` for $WinPlatform / $AppEngine |
| `RuntimeStatusUnknownTimeoutSeconds` | int | `15` | Grace period before an un-probed host is assumed Stopped |
**Galaxy repository** (`GalaxyRepositoryConfiguration.cs`):
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `ConnectionString` | string | `Server=localhost;Database=ZB;Integrated Security=true;` | ZB SQL Server connection |
| `ChangeDetectionIntervalSeconds` | int | `30` | Poll interval for `galaxy.time_of_last_deploy` |
| `CommandTimeoutSeconds` | int | `30` | SQL command timeout |
| `ExtendedAttributes` | bool | `false` | Include extended attribute metadata in discovery |
| `Scope` | enum (`Galaxy` \| `LocalPlatform`) | `Galaxy` | Address-space scope filter (commit bc282b6) |
| `PlatformName` | string? | `Environment.MachineName` | Platform to scope to when `Scope=LocalPlatform` |
**IPC** (new for v2):
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `PipeName` | string | `otopcua-galaxy-{InstanceId}` | Named pipe name |
| `HostStartupTimeoutMs` | int | `30000` | Proxy wait for Host `Ready` handshake |
| `IpcCallTimeoutMs` | int | `15000` | Per-call RPC timeout |
### Addressing
Galaxy objects carry two names:
- **`contained_name`** — human-readable, scoped to parent; used for OPC UA browse tree
- **`tag_name`** — globally unique system identifier; used for MXAccess runtime references
| Layer | Example |
|-------|---------|
| OPC UA browse path | `TestMachine_001/DelmiaReceiver/DownloadPath` |
| OPC UA NodeId | `ns=<galaxyNs>;s=<tagName>.<AttributeName>` |
| MXAccess reference | `DelmiaReceiver_001.DownloadPath` (passed to `AddItem()`) |
Tag discovery is **dynamic** — driven by the Galaxy repository DB (`gobject`, `dynamic_attribute`, `primitive_instance`, `template_definition`). Optional `Scope=LocalPlatform` filters the hierarchy via the `hosted_by_gobject_id` chain to the subtree rooted at the local $WinPlatform (on a dev Galaxy: 49→3 objects, 4206→386 attributes).
### Data Type Mapping (`MxDataTypeMapper.cs`, `gr/data_type_mapping.md`)
| mx_data_type | Galaxy Type | OPC UA BuiltInType | CLR Type |
|--------------|-------------|--------------------|----------|
| 1 | Boolean | Boolean (i=1) | `bool` |
| 2 | Integer | Int32 (i=6) | `int` |
| 3 | Float | Float (i=10) | `float` |
| 4 | Double | Double (i=11) | `double` |
| 5 | String | String (i=12) | `string` |
| 6 | Time | DateTime (i=13) | `DateTime` |
| 7 | ElapsedTime | Double (i=11) | `double` (seconds) |
| 8 | Reference | String (i=12) | `string` |
| 13 | Enumeration | Int32 (i=6) | `int` |
| 14 / 16 | Custom | String (i=12) | `string` |
| 15 | InternationalizedString | LocalizedText (i=21) | `string` |
| (default) | Unknown | String (i=12) | `string` |
**Arrays**: `is_array=0` → ValueRank `-1` (Scalar); `is_array=1` → ValueRank `1` (OneDimension), ArrayDimensions = `[array_dimension]`.
### Security Classification Mapping (`SecurityClassificationMapper.cs`)
| security_classification | Galaxy Level | OPC UA Write Permission |
|-------------------------|--------------|-------------------------|
| 0 | FreeAccess | `WriteOperate` |
| 1 | Operate | `WriteOperate` |
| 2 | SecuredWrite | — (read-only in v1) |
| 3 | VerifiedWrite | — (read-only in v1) |
| 4 | Tune | `WriteTune` |
| 5 | Configure | `WriteConfigure` |
| 6 | ViewOnly | — (read-only) |
Maps to the OPC UA roles `ReadOnly` / `WriteOperate` / `WriteTune` / `WriteConfigure` defined in the LDAP role provider (see `docs/security.md`).
### Subscription Model — Native MXAccess Advisories
**Galaxy is one of three drivers with native subscriptions (Galaxy, TwinCAT, OPC UA Client).** No polling.
- Mechanism: `LMXProxyServer.AddItem()``AdviseSupervisory(handle, itemHandle)`; callbacks delivered through the `ILMXProxyServerEvents.OnDataChange` COM event
- Callback signature: `MxDataChangeHandler(itemHandle, MXSTATUS_PROXY, value, quality, timestamp)`
- Dispatch: STA COM event → dispatch-thread queue → OPC UA `ClearChangeMasks` fan-out (decouples COM thread from UA stack lock — commit c76ab8f)
- **Stored subscriptions** replayed on reconnect via `ReplayStoredSubscriptionsAsync()`
- **Probe tag** + runtime-status probes provide connection-health visibility (see §14)
- **Bad-quality fan-out**: when a host ($WinPlatform or $AppEngine) ScanState transitions to Stopped, every attribute under that host is immediately published as `BadOutOfService` (commits 7310925, c76ab8f)
### Alarm Model
In-process alarm-condition tracking (v1 baseline; extended in v2 to match `IAlarmSource`):
- **Auto-subscribed attributes per alarm-eligible object**: `InAlarm`, `Priority`, `Description` (cached for severity and message)
- **Filtering**: `AlarmFilterConfiguration.ObjectFilters[]` — include/exclude by template chain (empty = all eligible)
- **Transitions**: `InAlarm` change → OPC UA A&C `AlarmConditionState` event (Active / Return to Normal)
- **Severity**: Galaxy `Priority` (1 = highest) mapped to OPC UA 11000 severity (higher = more severe)
- **Acknowledgment**: local OPC UA ack forwards to MXAccess write on the `Ack` attribute of the alarm-bearing object
### History Model — Wonderware Historian (optional plugin)
- Loaded **at runtime** from `ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dll` when `Historian.Enabled=true`; compile-time optional
- SDK: `aahClientManaged` / `aahClientCommon`
- Supported OPC UA HDA calls:
- `HistoryReadRawModified` (raw values with bounds)
- `HistoryReadProcessed` (Historian aggregates: AVG, MIN, MAX, TIMEAVG, etc. — mapped to OPC UA aggregates)
- Continuation points for paged reads
- Only attributes flagged `historize=1` in the Galaxy DB expose `AccessLevel.HistoryRead`
### Error Mapping — MXAccess → Quality → OPC UA StatusCode
**Byte quality (OPC DA convention)**`QualityMapper.cs`:
| OPC DA Quality | Category |
|----------------|----------|
| `>= 192` | Good |
| `64191` | Uncertain |
| `< 64` | Bad |
**MXAccess error codes → Quality** (`MxErrorCodes.cs`):
| Code | Name | Quality |
|------|------|---------|
| 1008 | `MX_E_InvalidReference` | `BadConfigError` |
| 1012 | `MX_E_WrongDataType` | `BadConfigError` |
| 1013 | `MX_E_NotWritable` | `BadOutOfService` |
| 1014 | `MX_E_RequestTimedOut` | `BadCommFailure` |
| 1015 | `MX_E_CommFailure` | `BadCommFailure` |
| 1016 | `MX_E_NotConnected` | `BadNotConnected` |
**Quality → OPC UA StatusCode** (`QualityMapper.cs`):
| Quality | StatusCode |
|---------|-----------|
| Good | `0x00000000` |
| GoodLocalOverride | `0x00D80000` |
| Uncertain | `0x40000000` |
| Bad (generic) | `0x80000000` |
| BadCommFailure | `0x80050000` |
| BadNotConnected | `0x808A0000` |
| BadOutOfService | `0x808D0000` |
### Change Detection
- `ChangeDetectionService` polls `galaxy.time_of_last_deploy` at `ChangeDetectionIntervalSeconds` (default 30s)
- On timestamp change, `OnGalaxyChanged` fires → Host re-queries hierarchy/attributes → emits `TagSetChanged` over IPC → Proxy implements `IRediscoverable` and rebuilds the affected subtree in the address space
- Platform-scope filter (commit bc282b6) applied during hierarchy load when `Scope=LocalPlatform`
### IPC Contract (Proxy ↔ Host) — `Galaxy.Shared`
.NET Standard 2.0 MessagePack contracts. Every request carries a correlation ID; responses carry the same ID plus success/error.
**Lifecycle / handshake**:
| Message | Direction | Payload |
|---------|-----------|---------|
| `ClientHello` | Proxy → Host | InstanceId, expected protocol version |
| `HostReady` | Host → Proxy | Host version, Galaxy name, capabilities |
| `Shutdown` | Proxy → Host | Graceful stop |
**Tag discovery** (`ITagDiscovery`):
| Message | Direction | Payload |
|---------|-----------|---------|
| `DiscoverHierarchyRequest` | Proxy → Host | `Scope`, `PlatformName` |
| `DiscoverHierarchyResponse` | Host → Proxy | `GalaxyObjectInfo[]` (TagName, ContainedName, ParentTagName, TemplateChain, category) |
| `DiscoverAttributesRequest` | Proxy → Host | `TagName[]` |
| `DiscoverAttributesResponse` | Host → Proxy | `GalaxyAttributeInfo[]` (Name, MxDataType, IsArray, ArrayDim, SecurityClass, Historized, WriteableRuntimeChecked) |
| `TagSetChangedNotification` | Host → Proxy | New deploy timestamp; triggers re-discover |
**Read / Write** (`IReadable`, `IWritable`):
| Message | Direction | Payload |
|---------|-----------|---------|
| `ReadRequest` | Proxy → Host | `TagRef[]` (tag_name + attribute) |
| `ReadResponse` | Host → Proxy | `VtqPayload[]` (value, quality, timestamp, statusCode) |
| `WriteRequest` | Proxy → Host | `(TagRef, Value, ExpectedDataType)[]` |
| `WriteResponse` | Host → Proxy | `(TagRef, StatusCode)[]` |
**Subscription** (`ISubscribable`):
| Message | Direction | Payload |
|---------|-----------|---------|
| `SubscribeRequest` | Proxy → Host | `TagRef[]` + Proxy-generated subscription ID |
| `SubscribeResponse` | Host → Proxy | Per-tag subscribe ack + handle |
| `UnsubscribeRequest` | Proxy → Host | handles |
| `DataChangeNotification` | Host → Proxy (push) | handle, VTQ, sequence number |
| `ProbeHealthNotification` | Host → Proxy (push) | probe tag staleness, `ScanState` transitions, overall connected/disconnected |
**Alarms** (`IAlarmSource`):
| Message | Direction | Payload |
|---------|-----------|---------|
| `AlarmEventNotification` | Host → Proxy (push) | source tag, InAlarm, Priority, Description, severity, transition type |
| `AlarmAckRequest` | Proxy → Host | source tag, user, comment |
**History** (`IHistoryProvider`):
| Message | Direction | Payload |
|---------|-----------|---------|
| `HistoryReadRawRequest` | Proxy → Host | TagRef, start, end, numValues, returnBounds, continuationPoint |
| `HistoryReadRawResponse` | Host → Proxy | values + next continuation point |
| `HistoryReadProcessedRequest` | Proxy → Host | TagRef, aggregateId, start, end, resampleInterval |
| `HistoryReadProcessedResponse` | Host → Proxy | aggregated values |
**Framing**: length-prefixed MessagePack frames over a single `NamedPipeServerStream` in `PipeTransmissionMode.Byte`. Separate outgoing pipe for push notifications or multiplex via message type tag.
### Threading / COM Constraints
- **STA thread** (`StaComThread.cs`) hosts MXAccess: `ApartmentState.STA`, raw Win32 `GetMessage` / `DispatchMessage` loop
- Work items marshaled in via `PostThreadMessage(WM_APP=0x8000)`
- **Per-handle serialization**: LMXProxyServer is not thread-safe — all Read/Write/Subscribe calls on one handle run serially via the STA queue
- **Dispatch thread** (separate from STA thread) drains `_pendingDataChanges` to the OPC UA framework; decouples the STA pump from UA stack locks so a slow subscriber can't back up COM event delivery
- **Reentrancy guards** — event unwiring must precede `Marshal.ReleaseComObject()` on disconnect
### Runtime Status (recent commits bc282b6 / 4b209f6 / 7310925 / c76ab8f / 0003984)
- `GalaxyRuntimeProbeManager` auto-subscribes `<ObjectName>.ScanState` for every $WinPlatform (category 1) and $AppEngine (category 3) in scope
- Per-host state machine: `Unknown → Running | Stopped`; transitions fire `_onHostStopped` / `_onHostRunning` callbacks on the dispatch thread
- **Synthetic OPC UA nodes** expose `ScanState` per host as read-only variables so clients see runtime topology without the dashboard
- **HealthCheck Rule 2e** monitors probe subscription health; a failed probe can no longer leave phantom entries that fan out false `BadOutOfService`
- Generalizes to the driver-agnostic `IHostConnectivityProbe` capability interface in v2 (see `plan.md` §5a)
### Implementation Notes
- **First Tier C out-of-process driver** — uses the `Galaxy.Proxy` / `Galaxy.Host` / `Galaxy.Shared` three-project split. The pattern is reusable; FOCAS is the second adopter (see §7), and any future driver with bitness, licensing, or stability-isolation needs reuses the same template. See `driver-stability.md` for the generalized contract
- `Galaxy.Proxy` (in the main server) implements `IDriver`, `ITagDiscovery`, `IRediscoverable`, `IReadable`, `IWritable`, `ISubscribable`, `IAlarmSource`, `IHistoryProvider`, `IHostConnectivityProbe`
- `Galaxy.Host` owns `MxAccessBridge`, `GalaxyRepository`, alarm tracking, `GalaxyRuntimeProbeManager`, and the Historian plugin — no reference to `Core.Abstractions`
- `Galaxy.Shared` is .NET Standard 2.0, referenced by both sides
- Existing v1 code is the implementation — **refactor in place** (extract capability interfaces first, then move behind IPC — see `plan.md` Decision #55)
- **Parity gate**: v2 driver must pass v1 `IntegrationTests` suite + scripted Client.CLI walkthrough before Phase 3 begins
### Operational Stability Notes
Galaxy has a Tier C deep dive in `driver-stability.md` covering the STA pump, COM object lifetime, subscription replay, recycle policy, and post-mortem contents. Driver-instance specifics:
- **Memory baseline scales with Galaxy size**. Watchdog floor of 200 MB above baseline + 1.5 GB hard ceiling — higher than FOCAS because legitimate Galaxy footprints are larger.
- **Slope tolerance is 5 MB/min** (more permissive than FOCAS) because address-space rebuild on redeploy can transiently allocate large amounts.
- **Known regression-prone failure modes** (closed in commits `c76ab8f` and `7310925`, must remain closed): phantom probe subscription flipping Tick() to Stopped; cross-host quality clear wiping sibling state during recovery; sync-over-async on the OPC UA stack thread; fire-and-forget alarm tasks racing shutdown. Each should have a regression test in the v2 parity suite.
- **STA pump health probe** every 10 s (separate from the proxy↔host heartbeat). A wedged pump is the most likely Tier C failure mode for Galaxy.
- **Recycle preserves cached `time_of_last_deploy` watermark** — the common case (crash unrelated to redeploy) skips full DB rediscovery for faster recovery.
### Namespace Assignment
Galaxy is the canonical **SystemPlatform-kind namespace** driver. It exposes Aveva System Platform / Galaxy objects as OPC UA — these are *processed* values with business meaning attached at Layer 3, not raw equipment signals. Per `plan.md` §4:
- The Galaxy driver's `DriverInstance.NamespaceId` must reference a `Namespace` row with `Kind = 'SystemPlatform'`.
- **UNS naming rules do NOT apply** to the Galaxy hierarchy. Tags belong to `DriverInstanceId + FolderPath` (v1 LmxOpcUa pattern preserved); `Tag.EquipmentId` is NULL.
- The Galaxy hierarchy reflects the gobject parent chain as v1 has always done — no migration to UNS path conventions in v2.
- If a future need arises to expose raw Galaxy gobject data alongside processed (e.g. an Aveva-Wonderware Historian raw signal feed), that becomes a *separate* driver instance assigned to an Equipment-kind namespace, with its own per-equipment mapping.
Galaxy (MXAccess) is a **Tier-A in-process driver** that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed `mxaccessgw` (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`). The gateway owns the MXAccess COM apartment, the STA pump, and the Galaxy Repository / Historian SDK on its own host; the driver itself is platform-agnostic and carries no COM or x86 bitness constraint. Project lives at `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`.
### Capability Surface
`GalaxyDriver` (in `GalaxyDriver.cs`) implements `IDriver`, `IDisposable`, plus six driver capabilities — eight interfaces total.
| Capability | Source files |
|------------|--------------|
| `ITagDiscovery` | `Browse/GalaxyDiscoverer.cs`, `Browse/GatewayGalaxyHierarchySource.cs`, `Browse/DataTypeMap.cs`, `Browse/SecurityMap.cs`, `Browse/AlarmRefBuilder.cs` |
| `IRediscoverable` | `Browse/DeployWatcher.cs`, `Browse/GatewayGalaxyDeployWatchSource.cs` |
| `IReadable` | `Runtime/GalaxyMxSession.cs`, `Runtime/MxValueDecoder.cs`, `Runtime/StatusCodeMap.cs` |
| `IWritable` | `Runtime/GatewayGalaxyDataWriter.cs` (+ `TracedGalaxyDataWriter.cs`), `Runtime/MxValueEncoder.cs` |
| `ISubscribable` | `Runtime/GatewayGalaxySubscriber.cs` (+ `TracedGalaxySubscriber.cs`), `Runtime/EventPump.cs`, `Runtime/SubscriptionRegistry.cs`, `Runtime/ReconnectSupervisor.cs` |
| `IHostConnectivityProbe` | `Health/HostStatusAggregator.cs`, `Health/HostConnectivityForwarder.cs`, `Health/PerPlatformProbeWatcher.cs` |
History reads + alarm condition tracking now live in the server-layer `IHistoryRouter` and `AlarmConditionService` (PR 7.2). Galaxy no longer carries `IHistoryProvider` or `IAlarmSource` of its own.
### DriverConfig JSON shape
Per `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`:
```jsonc
{
"Gateway": {
"Endpoint": "http://localhost:5120",
"ApiKeySecretRef": "secret:galaxy-gw-api-key",
"UseTls": true,
"CaCertificatePath": null,
"ConnectTimeoutSeconds": 10,
"DefaultCallTimeoutSeconds": 30,
"StreamTimeoutSeconds": 0
},
"MxAccess": {
"ClientName": "OtOpcUa",
"PublishingIntervalMs": 1000,
"WriteUserId": 0,
"EventPumpChannelCapacity": 50000
},
"Repository": {
"DiscoverPageSize": 5000,
"WatchDeployEvents": true
},
"Reconnect": {
"InitialBackoffMs": 500,
"MaxBackoffMs": 30000,
"ReplayOnSessionLost": true
}
}
```
`Gateway.ApiKeySecretRef` resolves through the server-side secret store (DPAPI in production, env override in dev) — the API key never appears in cleartext config. `MxAccess.ClientName` MUST be unique per OtOpcUa instance; redundancy pairs enforce uniqueness at install time. `StreamTimeoutSeconds = 0` keeps the `StreamEvents` RPC alive for the lifetime of the driver.
### Performance, tracing, soak
See [Galaxy.Performance.md](Galaxy.Performance.md) for the OpenTelemetry trace map, the per-RPC metric set (`galaxy.events.dropped`, channel headroom, reconnect backoff distribution), and the soak-run profile.
### Parity rig + gateway setup
See [Galaxy.ParityRig.md](Galaxy.ParityRig.md) and the `mxaccessgw` repo for the gateway worker layout and the dev-rig recipe.
---

View File

@@ -1,7 +1,6 @@
# FOCAS wire protocol — what's authoritative vs. what's guessed
Companion to [`focas-simulator-plan.md`](focas-simulator-plan.md). Written during
Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
Written during Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
wire protocol (can quote with confidence) from what we're *guessing* (will need
Wireshark traces to validate in Stream C).

View File

@@ -1,3 +1,14 @@
> **✅ Completed 2026-04-30 — historical record of Phase 2 (Galaxy out-of-process split).**
>
> Phase 2 produced the `Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared`
> three-project split as a stepping stone toward the eventual mxaccessgw
> architecture. Those projects shipped, served their purpose for
> roughly a year, then retired in PR 7.2 alongside the
> `OtOpcUaGalaxyHost` Windows service. This file is preserved as the
> phase-exit evidence; do not treat it as live architecture
> documentation. See `docs/drivers/Galaxy.md` for the current
> in-process driver.
# Phase 2 — Galaxy Out-of-Process Refactor (Tier C)
> **Status**: DRAFT — implementation plan for Phase 2 of the v2 build (`plan.md` §6, `driver-stability.md` §"Galaxy — Deep Dive").

View File

@@ -1,80 +0,0 @@
# PR 1 — Phase 1 + Phase 2 A/B/C → v2
**Source**: `phase-1-configuration` (commits `980ea51..7403b92`, 11 commits)
**Target**: `v2`
**URL**: https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/new/phase-1-configuration
## Summary
- **Phase 1 complete** — Configuration project with 16 entities + 3 EF migrations
(InitialSchema + 8 stored procs + AuthorizationGrants), Core + Server + full Admin UI
(Blazor Server with cluster CRUD, draft → diff → publish → rollback, equipment with
OPC 40010, UNS, namespaces, drivers, ACLs, reservations, audit), LDAP via GLAuth
(`localhost:3893`), SignalR real-time fleet status + alerts.
- **Phase 2 Streams A + B + C feature-complete** — full IPC contract surface
(Galaxy.Shared, netstandard2.0, MessagePack), Galaxy.Host with real Win32 STA pump,
ACL + caller-SID + per-process-secret IPC, Galaxy-specific MemoryWatchdog +
RecyclePolicy + PostMortemMmf + MxAccessHandle, three `IGalaxyBackend`
implementations (Stub / DbBacked / **MxAccess** — real ArchestrA.MxAccess.dll
reference, x86, smoke-tested live against `LMXProxyServer`), Galaxy.Proxy with all
9 capability interfaces (`IDriver` / `ITagDiscovery` / `IReadable` / `IWritable` /
`ISubscribable` / `IAlarmSource` / `IHistoryProvider` / `IRediscoverable` /
`IHostConnectivityProbe`) + supervisor (Backoff + CircuitBreaker +
HeartbeatMonitor).
- **Phase 2 Stream D non-destructive deliverables** — appsettings.json → DriverConfig
migration script, two-service Windows installer scripts, process-spawn cross-FX
parity test, Stream D removal procedure doc with both Option A (rewrite 494 v1
tests) and Option B (archive + new v2 E2E suite) spelled out step-by-step.
## What's NOT in this PR
- Legacy `OtOpcUa.Host` deletion (Stream D.1) — reserved for a follow-up PR after
Option B's E2E suite is green. The 494 v1 tests still pass against the unchanged
legacy Host.
- Live-Galaxy parity validation (Stream E) — needs the iterative debug cycle the
removal-procedure doc describes.
## Tests
**964 pass / 1 pre-existing Phase 0 baseline failure**, across 14 test projects:
| Project | Pass | Notes |
|---|---:|---|
| Core.Abstractions.Tests | 24 | |
| Configuration.Tests | 42 | incl. 7 schema compliance, 8 stored-proc, 3 SQL-role auth, 13 validator, 6 LiteDB cache, 5 generation-applier |
| Core.Tests | 4 | DriverHost lifecycle |
| Server.Tests | 2 | NodeBootstrap + LiteDB cache fallback |
| Admin.Tests | 21 | incl. 5 RoleMapper, 6 LdapAuth, 3 LiveLdap, 2 FleetStatusPoller, 2 services-integration |
| Driver.Galaxy.Shared.Tests | 6 | Round-trip + framing |
| Driver.Galaxy.Host.Tests | 30 | incl. 5 GalaxyRepository live ZB, 3 live MXAccess COM, 5 EndToEndIpc, 2 IpcHandshake, 4 MemoryWatchdog, 3 RecyclePolicy, 3 PostMortemMmf, 3 StaPump, 2 service-installer dry-run |
| Driver.Galaxy.Proxy.Tests | 10 | 9 unit + 1 process-spawn parity |
| Client.Shared.Tests | 131 | unchanged |
| Client.UI.Tests | 98 | unchanged |
| Client.CLI.Tests | 51 / 1 fail | pre-existing baseline failure |
| Historian.Aveva.Tests | 41 | unchanged |
| IntegrationTests (net48) | 6 | unchanged — v1 parity baseline |
| **OtOpcUa.Tests (net48)** | **494** | **unchanged — v1 parity baseline** |
## Test plan for reviewers
- [ ] `dotnet build ZB.MOM.WW.OtOpcUa.slnx` succeeds with no warnings beyond the
known NuGetAuditSuppress + xUnit1051 warnings
- [ ] `dotnet test ZB.MOM.WW.OtOpcUa.slnx` shows the same 964/1 result
- [ ] `Get-Service aaGR, aaBootstrap` reports Running on the merger's box
- [ ] `docker ps --filter name=otopcua-mssql` shows the SQL container Up
- [ ] Admin UI boots (`dotnet run --project src/ZB.MOM.WW.OtOpcUa.Admin`); home page
renders at http://localhost:5123/; LDAP sign-in with GLAuth `readonly` /
`readonly123` succeeds
- [ ] Migration script dry-run: `powershell -File
scripts/migration/Migrate-AppSettings-To-DriverConfig.ps1 -DryRun` produces
a well-formed DriverConfig JSON
- [ ] Spot-read three commit messages to confirm the deferred-with-rationale items
are explicitly documented (`549cd36`, `a7126ba`, `7403b92` are the most
recent and most detailed)
## Follow-up tracking
PR 2 (next session) will execute Stream D Option B — archive `OtOpcUa.Tests` as
`OtOpcUa.Tests.v1Archive`, build the new `OtOpcUa.Driver.Galaxy.E2E` test project,
delete legacy `OtOpcUa.Host`, and run the parity-validation cycle. See
`docs/v2/implementation/stream-d-removal-procedure.md`.

View File

@@ -1,69 +0,0 @@
# PR 2 — Phase 2 Stream D Option B (archive v1 + E2E suite) → v2
**Source**: `phase-2-stream-d` (branched from `phase-1-configuration`)
**Target**: `v2`
**URL** (after push): https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/new/phase-2-stream-d
## Summary
Phase 2 Stream D Option B per `docs/v2/implementation/stream-d-removal-procedure.md`:
- **Archived the v1 surface** without deleting:
- `tests/ZB.MOM.WW.OtOpcUa.Tests/``tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/`
(`<AssemblyName>` kept as `ZB.MOM.WW.OtOpcUa.Tests` so v1 Host's `InternalsVisibleTo`
still matches; `<IsTestProject>false</IsTestProject>` so solution test runs skip it).
- `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/``<IsTestProject>false</IsTestProject>`
+ archive comment.
- `src/ZB.MOM.WW.OtOpcUa.Host/` + `src/ZB.MOM.WW.OtOpcUa.Historian.Aveva/` — archive
PropertyGroup comments. Both still build (Historian plugin + 41 historian tests still
pass) so Phase 2 PR 3 can delete them in a focused, reviewable destructive change.
- **New `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/`** test project (.NET 10):
- `ParityFixture` spawns `OtOpcUa.Driver.Galaxy.Host.exe` (net48 x86) as a subprocess via
`Process.Start`, connects via real named pipe, exposes a connected `GalaxyProxyDriver`.
Skips when Galaxy ZB unreachable / Host EXE not built / Administrator shell.
- `HierarchyParityTests` (3) and `StabilityFindingsRegressionTests` (4) — one test per
2026-04-13 stability finding (phantom probe, cross-host quality clear, sync-over-async,
fire-and-forget alarm shutdown race).
- **`docs/v2/V1_ARCHIVE_STATUS.md`** — inventory + deletion plan for PR 3.
- **`docs/v2/implementation/exit-gate-phase-2-final.md`** — supersedes the two partial-exit
docs with the as-built state, adversarial review of PR 2 deltas (4 new findings), and the
recommended PR sequence (1 → 2 → 3 → 4).
## What's NOT in this PR
- Deletion of the v1 archive — saved for PR 3 with explicit operator review (destructive change).
- Wonderware Historian SDK plugin port — Task B.1.h, follow-up to enable real `HistoryRead`.
- MxAccess subscription push-frames — Task B.1.s, follow-up to enable real-time
data-change push from Host → Proxy.
## Tests
**`dotnet test ZB.MOM.WW.OtOpcUa.slnx`**: **470 pass / 7 skip / 1 pre-existing baseline**.
The 7 skips are the new E2E tests, all skipping with the documented reason
"PipeAcl denies Administrators on dev shells" — the production install runs as a non-admin
service account and these tests will execute there.
Run the archived v1 suites explicitly:
```powershell
dotnet test tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive # → 494 pass
dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests # → 6 pass
```
## Test plan for reviewers
- [ ] `dotnet build ZB.MOM.WW.OtOpcUa.slnx` succeeds with no warnings beyond the known
NuGetAuditSuppress + NU1702 cross-FX
- [ ] `dotnet test ZB.MOM.WW.OtOpcUa.slnx` shows the 470/7-skip/1-baseline result
- [ ] Both archived suites pass when run explicitly
- [ ] Build the Galaxy.Host EXE (`dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`),
then run E2E tests on a non-admin shell — they should actually execute and pass
against live Galaxy ZB
- [ ] Spot-read `docs/v2/V1_ARCHIVE_STATUS.md` and confirm the deletion plan is acceptable
## Follow-up tracking
- **PR 3** (next session, when ready): execute the deletion plan in `V1_ARCHIVE_STATUS.md`.
4 projects removed, .slnx updated, full solution test confirms parity.
- **PR 4** (Phase 2 follow-up): port Historian plugin + wire MxAccess subscription pushes +
close the high/medium open findings from `exit-gate-phase-2-final.md`.

View File

@@ -1,91 +0,0 @@
# PR 4 — Phase 2 follow-up: close the 4 open MXAccess findings
**Source**: `phase-2-pr4-findings` (branched from `phase-2-stream-d`)
**Target**: `v2`
## Summary
Closes the 4 high/medium open findings carried forward in `exit-gate-phase-2-final.md`:
- **High 1 — `ReadAsync` subscription-leak on cancel.** One-shot read now wraps the
subscribe→first-OnDataChange→unsubscribe pattern in a `try/finally` so the per-tag
callback is always detached, and if the read installed the underlying MXAccess
subscription itself (no other caller had it), it tears it down on the way out.
- **High 2 — No reconnect loop on the MXAccess COM connection.** New
`MxAccessClientOptions { AutoReconnect, MonitorInterval, StaleThreshold }` + a background
`MonitorLoopAsync` that watches a stale-activity threshold + probes the proxy via a
no-op COM call, then reconnects-with-replay (re-Register, re-AddItem every active
subscription) when the proxy is dead. Liveness signal: every `OnDataChange` callback bumps
`_lastObservedActivityUtc`. Defaults match v1 monitor cadence (5s poll, 60s stale).
`ReconnectCount` exposed for diagnostics; `ConnectionStateChanged` event for downstream
consumers (the supervisor on the Proxy side already surfaces this through its
HeartbeatMonitor, but the Host-side event lets local logging/metrics hook in).
- **Medium 3 — `MxAccessGalaxyBackend.SubscribeAsync` doesn't push OnDataChange frames back to
the Proxy.** New `IGalaxyBackend.OnDataChange` / `OnAlarmEvent` / `OnHostStatusChanged`
events that the new `GalaxyFrameHandler.AttachConnection` subscribes per-connection and
forwards as outbound `OnDataChangeNotification` / `AlarmEvent` /
`RuntimeStatusChange` frames through the connection's `FrameWriter`. `MxAccessGalaxyBackend`
fans out per-tag value changes to every `SubscriptionId` that's listening to that tag
(multiple Proxy subs may share a Galaxy attribute — single COM subscription, multi-fan-out
on the wire). Stub + DbBacked backends declare the events with `#pragma warning disable
CS0067` (treat-warnings-as-errors would otherwise fail on never-raised events that exist
only to satisfy the interface).
- **Medium 4 — `WriteValuesAsync` doesn't await `OnWriteComplete`.** New
`WriteAsync(...)` overload returns `bool` after awaiting the OnWriteComplete callback via
the v1-style `TaskCompletionSource`-keyed-by-item-handle pattern in `_pendingWrites`.
`MxAccessGalaxyBackend.WriteValuesAsync` now reports per-tag `Bad_InternalError` when the
runtime rejected the write, instead of false-positive `Good`.
## Pipe server change
`IFrameHandler` gains `AttachConnection(FrameWriter writer): IDisposable` so the handler can
register backend event sinks on each accepted connection and detach them at disconnect. The
`PipeServer.RunOneConnectionAsync` calls it after the Hello handshake and disposes it in the
finally of the per-connection scope. `StubFrameHandler` returns `IFrameHandler.NoopAttachment.Instance`
(net48 doesn't support default interface methods, so the empty-attach lives as a public nested
class).
## Tests
**`dotnet test ZB.MOM.WW.OtOpcUa.slnx`**: **460 pass / 7 skip (E2E on admin shell) / 1
pre-existing baseline failure**. No regressions. The Driver.Galaxy.Host unit tests + 5 live
ZB smoke + 3 live MXAccess COM smoke all pass unchanged.
## Test plan for reviewers
- [ ] `dotnet build` clean
- [ ] `dotnet test` shows 460/7-skip/1-baseline
- [ ] Spot-check `MxAccessClient.MonitorLoopAsync` against v1's `MxAccessClient.Monitor`
partial (`src/ZB.MOM.WW.OtOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs`) — same
polling cadence, same probe-then-reconnect-with-replay shape
- [ ] Read `GalaxyFrameHandler.ConnectionSink.Dispose` and confirm event handlers are
detached on connection close (no leaked invocation list refs)
- [ ] `WriteValuesAsync` returning `Bad_InternalError` on a runtime-rejected write is the
correct shape — confirm against the v1 `MxAccessClient.ReadWrite.cs` pattern
## What's NOT in this PR
- Wonderware Historian SDK plugin port (Task B.1.h) — separate PR, larger scope.
- Alarm subsystem wire-up (`MxAccessGalaxyBackend.SubscribeAlarmsAsync` is still a no-op).
`OnAlarmEvent` is declared on the backend interface and pushed by the frame handler when
raised; `MxAccessGalaxyBackend` just doesn't raise it yet (waits for the alarm-tracking
port from v1's `AlarmObjectFilter` + Galaxy alarm primitives).
- Host-status push (`OnHostStatusChanged`) — declared on the interface and pushed by the
frame handler; `MxAccessGalaxyBackend` doesn't raise it (the Galaxy.Host's
`HostConnectivityProbe` from v1 needs porting too, scoped under the Historian PR).
## Adversarial review
Quick pass over the PR 4 deltas. No new findings beyond:
- **Low 1** — `MonitorLoopAsync`'s `$Heartbeat` probe item-handle is leaked
(`AddItem` succeeds, never `RemoveItem`'d). Cosmetic — the probe item is internal to
the COM connection, dies with `Unregister` at disconnect/recycle. Worth a follow-up
to call `RemoveItem` after the probe succeeds.
- **Low 2** — Replay loop in `MonitorLoopAsync` swallows per-subscription failures. If
Galaxy permanently rejects a previously-valid reference (rare but possible after a
re-deploy), the user gets silent data loss for that one subscription. The stub-handler-
unaware operator wouldn't notice. Worth surfacing as a `ConnectionStateChanged(false)
→ ConnectionStateChanged(true)` payload that includes the replay-failures list.
Both are low-priority follow-ups, not PR 4 blockers.

View File

@@ -70,6 +70,17 @@ integration tests until reproduced on hardware:
- TxId drop under load (forum rumour; not reproduced).
- Pre-2004 firmware ABCD word order (every shipped DL205/DL260 since 2004 is CDAB).
### Siemens SIMATIC S7
Quirk catalog at [`s7.md`](s7.md) — covers S7-1200 / S7-1500 / S7-300 / S7-400 /
ET 200SP. Modbus TCP isn't native; each platform exposes it via a different
add-on module with its own register-mapping conventions.
### Mitsubishi MELSEC
Quirk catalog at [`mitsubishi.md`](mitsubishi.md) — Modbus TCP via add-on modules
across the MELSEC family.
### Future devices
One section per device class, same shape as DL205. Quirks that apply across

View File

@@ -1,51 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Purpose
The goal of this project is to identify and develop SQL queries that extract the Galaxy object hierarchy from the **System Platform Galaxy Repository** database in order to build a tag structure for an OPC UA server.
Specifically, we need to:
- Build the hierarchy of **areas** and **automation objects** (using contained names for human-readable browsing)
- Translate contained names to **tag_names** for read/write operations (e.g., `TestMachine_001.DelmiaReceiver` in the hierarchy becomes `DelmiaReceiver_001` when addressing tag values)
See `layout.md` for details on the hierarchy vs tag name relationship.
## Key Files
### Documentation
- `connectioninfo.md` — Database connection details and sqlcmd usage
- `layout.md` — Galaxy object hierarchy, contained_name vs tag_name translation, and target OPC UA structure
- `build_layout_plan.md` — Step-by-step plan for extracting hierarchy, attaching attributes, and monitoring for changes
- `data_type_mapping.md` — Galaxy mx_data_type to OPC UA DataType mapping, including array handling (ValueRank, ArrayDimensions)
### Queries
- `queries/hierarchy.sql` — Deployed object hierarchy with browse names and parent relationships
- `queries/attributes.sql` — User-defined (dynamic) attributes with data types and array dimensions
- `queries/attributes_extended.sql` — All attributes (system + user-defined) with data types and array dimensions
- `queries/change_detection.sql` — Poll `galaxy.time_of_last_deploy` to detect deployment changes
### Schema Reference
- `schema.md` — Full schema reference for all tables and views in the ZB database
- `ddl/tables/` — Individual CREATE TABLE definitions
- `ddl/views/` — Individual view definitions
## Working with the Galaxy Repository Database
The Galaxy Repository is the backing SQL Server database for Wonderware/AVEVA System Platform (Galaxy: ZB, localhost, Windows Auth). Key tables used by the queries:
- **gobject** — Object instances, hierarchy (contained_by_gobject_id, area_gobject_id), deployment state (deployed_package_id)
- **template_definition** — Object type categories (category_id distinguishes areas, engines, user-defined objects, etc.)
- **dynamic_attribute** — User-defined attributes on templates, inherited by instances via derived_from_gobject_id chain
- **attribute_definition** — System/primitive attributes
- **primitive_instance** — Links objects to their primitive components and attribute definitions
- **galaxy** — Single-row table with time_of_last_deploy for change detection
Use `sqlcmd -S localhost -d ZB -E -Q "..."` to run queries. See `connectioninfo.md` for details.
## Conventions
- Store all connection parameters in `connectioninfo.md`, not scattered across scripts.
- Keep SQL query examples and extraction notes as Markdown files in this repo.
- If scripts are added (Python, PowerShell, etc.), document their usage and dependencies alongside them.

View File

@@ -1,84 +0,0 @@
# OPC UA Server Layout — Build Plan
## Overview
Extract the Galaxy object hierarchy and tag definitions from the ZB (Galaxy Repository) database to construct an OPC UA server address space. The root node is hardcoded as **ZB**.
## Step 1: Build the Browse Tree
Run `queries/hierarchy.sql` to get all deployed automation objects and their parent-child relationships.
For each row returned:
- `parent_gobject_id = 0` → child of the root ZB node
- `is_area = 1` → create as an OPC UA folder node (organizational)
- `is_area = 0` → create as an OPC UA object node (container for tags)
- Use `browse_name` as the OPC UA BrowseName/DisplayName
- Store `gobject_id` and `tag_name` for attribute lookup and tag reference translation
Build the tree by matching each row's `parent_gobject_id` to another row's `gobject_id`. The result is:
```
ZB (root, hardcoded)
└── DEV (folder, is_area=1)
├── DevAppEngine (object)
├── DevPlatform (object)
└── TestArea (folder, is_area=1)
├── DevTestObject (object)
└── TestMachine_001 (object)
├── DelmiaReceiver (object, browse_name from contained_name)
└── MESReceiver (object, browse_name from contained_name)
```
## Step 2: Attach Attributes as Tag Nodes
Run `queries/attributes.sql` to get all user-defined attributes for deployed objects.
For each attribute row:
- Match to the browse tree via `gobject_id`
- Create an OPC UA variable node under the matching object node
- Use `attribute_name` as the BrowseName/DisplayName
- Use `full_tag_reference` as the runtime tag path for read/write operations
- Map `mx_data_type` to OPC UA built-in types:
| mx_data_type | Description | OPC UA Type |
|--------------|-------------|-------------|
| 1 | Boolean | Boolean |
| 2 | Integer | Int32 |
| 3 | Float | Float |
| 4 | Double | Double |
| 5 | String | String |
| 6 | Time | DateTime |
| 7 | ElapsedTime | Double (seconds) or Duration |
- If `is_array = 1`, create the variable as an array with rank 1 and dimension from `array_dimension`
## Step 3: Monitor for Changes
Poll `queries/change_detection.sql` on a regular interval (e.g., every 30 seconds).
```
SELECT time_of_last_deploy FROM galaxy;
```
Compare the returned `time_of_last_deploy` to the last known value:
- **No change** → do nothing
- **Changed** → a deployment occurred; re-run Steps 1 and 2 to rebuild the address space
This handles objects being deployed, undeployed, added, or removed.
## Connection Details
See `connectioninfo.md` for database connection parameters and sqlcmd usage.
```
sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE"
```
## Query Files
| File | Purpose |
|------|---------|
| `queries/hierarchy.sql` | Deployed object hierarchy with browse names and parent relationships |
| `queries/attributes.sql` | User-defined attributes with data types and array dimensions |
| `queries/attributes_extended.sql` | All attributes (system + user-defined) with data types and array dimensions |
| `queries/change_detection.sql` | Poll galaxy.time_of_last_deploy for deployment changes |

View File

@@ -1,26 +0,0 @@
# Galaxy Repository — Connection Information
## Database Connection
| Parameter | Value |
|-----------------|----------------|
| Server | localhost (default instance) |
| Database Name | ZB |
| Port | 1433 (default) |
| Authentication | Windows Auth |
| Username | dohertj2 |
## sqlcmd Usage
```
sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE"
```
- `-S localhost` — default instance
- `-d ZB` — database name
- `-E` — Windows Authentication (dohertj2)
## Notes
- The Galaxy Repository is a SQL Server database created and managed by AVEVA System Platform (formerly Wonderware).
- Typically accessed via SQL Server Management Studio (SSMS), `sqlcmd`, or programmatically via ODBC/ADO.NET/pyodbc.

View File

@@ -1,96 +0,0 @@
# Data Type Mapping — Galaxy Repository to OPC UA
## Scalar Type Mapping
| mx_data_type | Galaxy Description | OPC UA DataType | OPC UA NodeId | Notes |
|--------------|--------------------|-----------------|---------------|-------|
| 1 | Boolean | Boolean | i=1 | Direct mapping |
| 2 | Integer (Int32) | Int32 | i=6 | Galaxy integers are 32-bit signed |
| 3 | Float (Single) | Float | i=10 | 32-bit IEEE 754 |
| 4 | Double | Double | i=11 | 64-bit IEEE 754 |
| 5 | String | String | i=12 | Unicode string |
| 6 | Time (DateTime) | DateTime | i=13 | Galaxy DateTime to OPC UA DateTime (100ns ticks since 1601-01-01) |
| 7 | ElapsedTime (TimeSpan) | Double | i=11 | No native OPC UA TimeSpan; map to Double representing seconds (or use Duration type alias, NodeId i=290) |
| 8 | (reference) | String | i=12 | Object reference; expose as string representation |
| 13 | (enumeration) | Int32 | i=6 | Enum backing value is integer |
| 14 | (custom) | String | i=12 | Fallback to string |
| 15 | InternationalizedString | LocalizedText | i=21 | OPC UA LocalizedText supports locale + text pairs |
| 16 | (custom) | String | i=12 | Fallback to string |
## OPC UA Built-in Type Reference
For context, the full set of OPC UA built-in types and their NodeIds:
| NodeId | Type | Description |
|--------|------|-------------|
| i=1 | Boolean | True/false |
| i=2 | SByte | Signed 8-bit integer |
| i=3 | Byte | Unsigned 8-bit integer |
| i=4 | Int16 | Signed 16-bit integer |
| i=5 | UInt16 | Unsigned 16-bit integer |
| i=6 | Int32 | Signed 32-bit integer |
| i=7 | UInt32 | Unsigned 32-bit integer |
| i=8 | Int64 | Signed 64-bit integer |
| i=9 | UInt64 | Unsigned 64-bit integer |
| i=10 | Float | 32-bit IEEE 754 |
| i=11 | Double | 64-bit IEEE 754 |
| i=12 | String | Unicode string |
| i=13 | DateTime | Date and time (100ns ticks since 1601-01-01) |
| i=14 | Guid | 128-bit globally unique identifier |
| i=15 | ByteString | Sequence of bytes |
| i=21 | LocalizedText | Locale + text pair |
## Array Handling
When `is_array = 1` in the attributes query, the OPC UA variable node must be configured as an array.
### ValueRank
Set on the OPC UA variable node to indicate scalar vs array:
| is_array | ValueRank | Meaning |
|----------|-----------|---------|
| 0 | -1 (Scalar) | Value is not an array |
| 1 | 1 (OneDimension) | Value is a one-dimensional array |
### ArrayDimensions
When `ValueRank = 1`, set the `ArrayDimensions` attribute to a single-element array containing the `array_dimension` value from the attributes query.
Example for `MESReceiver_001.MoveInPartNumbers` (`is_array=1`, `array_dimension=50`):
- DataType: String (i=12)
- ValueRank: 1
- ArrayDimensions: [50]
Example for `TestMachine_001.MachineID` (`is_array=0`):
- DataType: String (i=12)
- ValueRank: -1
- ArrayDimensions: (not set)
## Security Classification
Galaxy attributes have a `security_classification` column that controls the access level required for writes. The attributes query returns this value for each attribute.
| security_classification | Galaxy Level | OPC UA Access | Description |
|-------------------------|--------------|---------------|-------------|
| 0 | FreeAccess | ReadWrite | No security restrictions |
| 1 | Operate | ReadWrite | Normal operating level (default) |
| 2 | SecuredWrite | ReadOnly | Requires elevated write access |
| 3 | VerifiedWrite | ReadOnly | Requires verified/confirmed write access |
| 4 | Tune | ReadWrite | Tuning-level access |
| 5 | Configure | ReadWrite | Configuration-level access |
| 6 | ViewOnly | ReadOnly | Read-only, no writes permitted |
Most attributes default to `Operate` (1). Higher values indicate more restrictive write access. `ViewOnly` (6) attributes should be exposed as read-only in OPC UA (`AccessLevel = CurrentRead` only, no `CurrentWrite`).
## DateTime Conversion
Galaxy `Time` (mx_data_type=6) stores DateTime values. OPC UA DateTime is defined as the number of 100-nanosecond intervals since January 1, 1601 (UTC). Ensure the conversion accounts for:
- Timezone: Galaxy may store local time; OPC UA expects UTC
- Epoch difference: adjust if Galaxy uses a different epoch (e.g., Unix epoch 1970-01-01)
## ElapsedTime Handling
Galaxy `ElapsedTime` (mx_data_type=7) represents a duration/timespan. OPC UA has no native TimeSpan type. Options:
- **Double (i=11)**: Store as seconds (recommended for simplicity)
- **Duration (i=290)**: OPC UA type alias for Double, semantically represents milliseconds — use if the OPC UA SDK supports it

View File

@@ -1,13 +0,0 @@
-- Table: ConversionQueue
CREATE TABLE [ConversionQueue] (
[id] int NULL,
[Name] nvarchar(329) NULL,
[IsCheckedOut] bit NOT NULL,
[Status] bit NOT NULL DEFAULT ((0)),
[MetaData] nchar(256) NULL,
[OperationType] nchar(20) NOT NULL,
[timestamp_of_last_change] bigint NULL,
[change_type] int NULL
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: CurrentSessionContainedName
CREATE TABLE [CurrentSessionContainedName] (
[Uniqeid] int NOT NULL,
[obj_id] int NULL,
[containedname] nvarchar(32) NULL,
CONSTRAINT [PK_CurrentSessionContainedName] PRIMARY KEY ([Uniqeid])
);
GO

View File

@@ -1,7 +0,0 @@
-- Table: ImportTransaction
CREATE TABLE [ImportTransaction] (
[ImportOperationId] nvarchar(329) NULL,
[Status] bit NOT NULL DEFAULT ((1))
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: aa_sql_objects
CREATE TABLE [aa_sql_objects] (
[object_name] nvarchar(128) NOT NULL,
[object_type] nvarchar(10) NOT NULL,
CONSTRAINT [PK_aa_sql_objects] PRIMARY KEY ([object_name])
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: affected_overview_symbols
CREATE TABLE [affected_overview_symbols] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[visual_element_id] int NOT NULL
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: alarm_message_defaults
CREATE TABLE [alarm_message_defaults] (
[phrase_id] int NOT NULL,
[default_message] nvarchar(1024) NOT NULL,
CONSTRAINT [PK_alarm_message_defaults] PRIMARY KEY ([phrase_id])
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: alarm_message_timestamps
CREATE TABLE [alarm_message_timestamps] (
[gobject_id] int NOT NULL,
[timestamp_of_populate] bigint NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_alarm_message_timestamps] PRIMARY KEY ([gobject_id])
);
GO

View File

@@ -1,12 +0,0 @@
-- Table: alarm_message_translations
CREATE TABLE [alarm_message_translations] (
[phrase_id] int NOT NULL,
[locale_id] smallint NOT NULL,
[translated_message] nvarchar(1024) NOT NULL,
CONSTRAINT [PK_alarm_message_translations] PRIMARY KEY ([phrase_id], [locale_id], [phrase_id], [locale_id])
);
GO
ALTER TABLE [alarm_message_translations] ADD FOREIGN KEY ([locale_id]) REFERENCES [supported_locales] ([locale_id]);
GO

View File

@@ -1,13 +0,0 @@
-- Table: alarm_messages
CREATE TABLE [alarm_messages] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[phrase_id] int NOT NULL,
CONSTRAINT [PK_alarm_messages] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [phrase_id], [gobject_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id])
);
GO
ALTER TABLE [alarm_messages] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]);
GO

View File

@@ -1,24 +0,0 @@
-- Table: attribute_definition
CREATE TABLE [attribute_definition] (
[attribute_definition_id] int NOT NULL,
[primitive_definition_id] int NOT NULL,
[attribute_name] nvarchar(329) NOT NULL,
[mx_attribute_id] smallint NOT NULL,
[has_config_set_handler] bit NOT NULL,
[mx_data_type] smallint NOT NULL,
[is_array] bit NOT NULL,
[security_classification] smallint NOT NULL,
[security_classification_needs_deployed] bit NOT NULL,
[mx_attribute_category] int NOT NULL,
[is_frequently_accessed] bit NOT NULL,
[is_locked] bit NOT NULL,
[is_locked_needs_deployed] bit NOT NULL,
[mx_value] text(2147483647) NOT NULL,
[mx_value_needs_deployed] bit NOT NULL,
CONSTRAINT [PK_attribute_definition] PRIMARY KEY ([primitive_definition_id], [mx_attribute_id], [primitive_definition_id])
);
GO
ALTER TABLE [attribute_definition] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
GO

View File

@@ -1,26 +0,0 @@
-- Table: attribute_reference
CREATE TABLE [attribute_reference] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[referring_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
[referring_mx_attribute_id] smallint NOT NULL DEFAULT ((0)),
[element_index] smallint NOT NULL DEFAULT ((0)),
[resolved_gobject_id] int NOT NULL DEFAULT ((0)),
[reference_string] nvarchar(700) NOT NULL DEFAULT (''),
[context_string] nvarchar(329) NOT NULL DEFAULT (''),
[object_signature] int NOT NULL DEFAULT ((0)),
[resolved_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
[resolved_mx_attribute_id] smallint NOT NULL DEFAULT ((0)),
[resolved_mx_property_id] smallint NOT NULL DEFAULT ((0)),
[attribute_signature] int NOT NULL DEFAULT ((0)),
[lock_type] int NOT NULL DEFAULT ((0)),
[is_valid] bit NOT NULL DEFAULT ((0)),
[attr_res_status] int NOT NULL DEFAULT ((0)),
[attribute_index] smallint NULL DEFAULT ((-1)),
CONSTRAINT [PK_attribute_reference] PRIMARY KEY ([gobject_id], [package_id], [referring_mx_primitive_id], [referring_mx_attribute_id], [element_index], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id])
);
GO
ALTER TABLE [attribute_reference] ADD FOREIGN KEY ([referring_mx_primitive_id]) REFERENCES [primitive_instance] ([package_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: attributes_translation_table
CREATE TABLE [attributes_translation_table] (
[gobject_id] int NULL,
[attribute_name] nvarchar(329) NOT NULL,
[new_primitive_id] int NULL,
[new_attribute_id] int NULL,
[old_primitive_id] int NULL,
[old_attribute_id] int NULL
);
GO

View File

@@ -1,11 +0,0 @@
-- Table: autobind_device
CREATE TABLE [autobind_device] (
[dio_id] int NOT NULL,
[overridden_naming_rule_id] int NULL,
CONSTRAINT [PK_autobind_device] PRIMARY KEY ([dio_id], [overridden_naming_rule_id], [dio_id])
);
GO
ALTER TABLE [autobind_device] ADD FOREIGN KEY ([dio_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: autobind_device_category
CREATE TABLE [autobind_device_category] (
[category_id] smallint NOT NULL,
[rule_id] int NULL DEFAULT ((0)),
CONSTRAINT [PK_autobind_device_category] PRIMARY KEY ([category_id], [rule_id], [category_id])
);
GO
ALTER TABLE [autobind_device_category] ADD FOREIGN KEY ([category_id]) REFERENCES [lookup_category] ([category_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: autobind_device_template
CREATE TABLE [autobind_device_template] (
[template_definition_id] int NOT NULL,
[rule_id] int NULL,
CONSTRAINT [PK_autobind_device_template] PRIMARY KEY ([template_definition_id], [rule_id], [template_definition_id])
);
GO
ALTER TABLE [autobind_device_template] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]);
GO

View File

@@ -1,13 +0,0 @@
-- Table: autobind_device_topic
CREATE TABLE [autobind_device_topic] (
[dio_id] int NOT NULL,
[sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
[overridden_naming_rule_id] int NULL,
[default_xlate_rule_id] int NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_autobind_device_topic] PRIMARY KEY ([dio_id], [sg_mx_primitive_id], [overridden_naming_rule_id], [dio_id])
);
GO
ALTER TABLE [autobind_device_topic] ADD FOREIGN KEY ([dio_id]) REFERENCES [autobind_device] ([dio_id]);
GO

View File

@@ -1,8 +0,0 @@
-- Table: autobind_naming_rule
CREATE TABLE [autobind_naming_rule] (
[rule_id] int NOT NULL,
[rule_name] nvarchar(329) NOT NULL,
CONSTRAINT [PK_autobind_naming_rule] PRIMARY KEY ([rule_id])
);
GO

View File

@@ -1,12 +0,0 @@
-- Table: autobind_naming_rule_spec
CREATE TABLE [autobind_naming_rule_spec] (
[rule_id] int NOT NULL,
[io_type] nchar(1) NOT NULL,
[rule_spec] nvarchar(512) NOT NULL,
CONSTRAINT [PK_autobind_naming_rule_spec] PRIMARY KEY ([rule_id], [io_type], [rule_id])
);
GO
ALTER TABLE [autobind_naming_rule_spec] ADD FOREIGN KEY ([rule_id]) REFERENCES [autobind_naming_rule] ([rule_id]);
GO

View File

@@ -1,10 +0,0 @@
-- Table: autobind_translation_rule
CREATE TABLE [autobind_translation_rule] (
[xlate_rule_id] int NOT NULL,
[xlate_rule_name] nvarchar(329) NOT NULL,
[xlate_rule_gsub_str] nvarchar(1000) NULL,
[xlate_rule_scope_global] bit NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_autobind_translation_rule] PRIMARY KEY ([xlate_rule_id])
);
GO

View File

@@ -1,17 +0,0 @@
-- Table: autobound_attribute
CREATE TABLE [autobound_attribute] (
[dio_id] int NOT NULL,
[sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
[gobject_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[mx_attribute_id] smallint NOT NULL,
[element_index] smallint NOT NULL DEFAULT ((0)),
[attr_alias] nvarchar(329) NULL,
[xlate_rule_id] int NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_autobound_attribute] PRIMARY KEY ([gobject_id], [mx_primitive_id], [mx_attribute_id], [element_index], [dio_id], [sg_mx_primitive_id], [dio_id], [sg_mx_primitive_id], [xlate_rule_id])
);
GO
ALTER TABLE [autobound_attribute] ADD FOREIGN KEY ([xlate_rule_id]) REFERENCES [autobind_translation_rule] ([xlate_rule_id]);
GO

View File

@@ -1,9 +0,0 @@
-- Table: client_control_class_link
CREATE TABLE [client_control_class_link] (
[gobject_id] int NOT NULL,
[file_id] int NULL,
[class_name] nvarchar(1024) NOT NULL,
CONSTRAINT [PK_client_control_class_link] PRIMARY KEY ([gobject_id])
);
GO

View File

@@ -1,11 +0,0 @@
-- Table: client_info
CREATE TABLE [client_info] (
[id] int NOT NULL,
[client_unique_identifier] nvarchar(4000) NOT NULL,
[client_name] nvarchar(64) NOT NULL,
[deployed_files_count] smallint NOT NULL,
[time_of_last_deployed_object_components] datetime NULL DEFAULT (getdate()),
[timestamp_of_last_synchronized] bigint NOT NULL DEFAULT ((0))
);
GO

View File

@@ -1,16 +0,0 @@
-- Table: control_index
CREATE TABLE [control_index] (
[entity_id] int NOT NULL,
[gobject_id] int NOT NULL,
[control_id] nvarchar(329) NULL,
[control_name] nvarchar(329) NOT NULL,
[control_description] nvarchar(2000) NULL,
[properties] nvarchar(-1) NULL,
[thumbnail] nvarchar(-1) NULL,
CONSTRAINT [PK_control_index] PRIMARY KEY ([gobject_id], [control_name], [gobject_id])
);
GO
ALTER TABLE [control_index] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,9 +0,0 @@
-- Table: data_type
CREATE TABLE [data_type] (
[mx_data_type] tinyint NOT NULL,
[description] varchar(30) NOT NULL,
[ow_data_type] varchar(10) NULL,
CONSTRAINT [PK_data_type] PRIMARY KEY ([mx_data_type])
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: deleted_gobject
CREATE TABLE [deleted_gobject] (
[gobject_id] int NOT NULL DEFAULT ((0)),
[timestamp_of_delete] timestamp NOT NULL,
CONSTRAINT [PK_deleted_gobject] PRIMARY KEY ([timestamp_of_delete])
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: deleted_ids
CREATE TABLE [deleted_ids] (
[table_id] smallint NULL,
[deleted_id] int NOT NULL,
[deletion_timestamp] timestamp NOT NULL,
[deletion_time] datetime NULL
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: deleted_visual_element
CREATE TABLE [deleted_visual_element] (
[visual_element_name] nvarchar(329) NULL,
[visual_element_type] nvarchar(32) NULL,
[timestamp_of_delete] timestamp NOT NULL
);
GO

View File

@@ -1,13 +0,0 @@
-- Table: deleted_visual_element_version
CREATE TABLE [deleted_visual_element_version] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[visual_element_name] nvarchar(329) NOT NULL,
[visual_element_type] nvarchar(32) NOT NULL,
[timestamp_of_delete] timestamp NOT NULL,
[visual_element_id] int NOT NULL,
CONSTRAINT [PK_deleted_visual_element_version] PRIMARY KEY ([gobject_id], [package_id], [timestamp_of_delete])
);
GO

View File

@@ -1,19 +0,0 @@
-- Table: deployed_file
CREATE TABLE [deployed_file] (
[deployed_file_id] int NOT NULL,
[file_id] int NOT NULL,
[node_name] nvarchar(256) NOT NULL,
[need_to_delete] int NOT NULL DEFAULT ((0)),
[is_package_deployed] bit NOT NULL,
[is_editor_deployed] bit NOT NULL,
[is_runtime_deployed] bit NOT NULL,
[is_browser_deployed] bit NOT NULL,
[file_version] nvarchar(50) NOT NULL DEFAULT (''),
[file_modified_time] nvarchar(50) NOT NULL DEFAULT (''),
CONSTRAINT [PK_deployed_file] PRIMARY KEY ([deployed_file_id], [file_id])
);
GO
ALTER TABLE [deployed_file] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
GO

View File

@@ -1,8 +0,0 @@
-- Table: deployed_intouch_viewapp
CREATE TABLE [deployed_intouch_viewapp] (
[timestamp_of_deploy] bigint NOT NULL DEFAULT ((1)),
[gobject_id] int NOT NULL,
[deploy_file_transfering] bit NULL DEFAULT ((0))
);
GO

View File

@@ -1,7 +0,0 @@
-- Table: deployed_intouch_viewapp_visual_element_dependency
CREATE TABLE [deployed_intouch_viewapp_visual_element_dependency] (
[gobject_id] int NULL,
[visual_element_name] nvarchar(2000) NULL
);
GO

View File

@@ -1,25 +0,0 @@
-- Table: dynamic_attribute
CREATE TABLE [dynamic_attribute] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[mx_attribute_id] smallint NOT NULL,
[attribute_name] nvarchar(329) NOT NULL,
[mx_data_type] smallint NOT NULL,
[is_array] bit NOT NULL,
[security_classification] smallint NOT NULL,
[mx_attribute_category] int NOT NULL,
[lock_type] int NOT NULL,
[mx_value] text(2147483647) NOT NULL,
[owned_by_gobject_id] int NOT NULL DEFAULT ((0)),
[original_lock_type] int NOT NULL DEFAULT ((0)),
[dynamic_attribute_type] smallint NOT NULL DEFAULT ((0)),
[bitvalues] smallint NOT NULL DEFAULT ((0)),
[dynamic_attribute_id] bigint NOT NULL,
CONSTRAINT [PK_dynamic_attribute] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [mx_attribute_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id])
);
GO
ALTER TABLE [dynamic_attribute] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]);
GO

View File

@@ -1,12 +0,0 @@
-- Table: external_content_media_types
CREATE TABLE [external_content_media_types] (
[entity_id] int NOT NULL,
[media_type] nvarchar(255) NOT NULL,
[control_entity_id] int NOT NULL,
[uri_property_name] nvarchar(1023) NULL,
[media_type_property_name] nvarchar(1023) NULL,
[is_default] bit NULL,
CONSTRAINT [PK_external_content_media_types] PRIMARY KEY ([entity_id])
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: feature
CREATE TABLE [feature] (
[feature_id] int NOT NULL,
[feature_name] nvarchar(256) NOT NULL,
[feature_type] nvarchar(256) NOT NULL,
CONSTRAINT [PK_feature] PRIMARY KEY ([feature_id])
);
GO

View File

@@ -1,11 +0,0 @@
-- Table: feature_file_link
CREATE TABLE [feature_file_link] (
[feature_id] int NOT NULL,
[file_id] int NOT NULL,
CONSTRAINT [PK_feature_file_link] PRIMARY KEY ([feature_id], [file_id], [feature_id], [file_id])
);
GO
ALTER TABLE [feature_file_link] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
GO

View File

@@ -1,13 +0,0 @@
-- Table: file_browserinfo_link
CREATE TABLE [file_browserinfo_link] (
[primitive_definition_id] int NOT NULL,
[file_id] int NOT NULL,
[assembly_strong_name] nvarchar(512) NOT NULL,
[assembly_type_name] nvarchar(256) NOT NULL,
CONSTRAINT [PK_file_browserinfo_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id])
);
GO
ALTER TABLE [file_browserinfo_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: file_pending_update
CREATE TABLE [file_pending_update] (
[file_id] int NOT NULL,
[node_name] nvarchar(256) NOT NULL,
CONSTRAINT [PK_file_pending_update] PRIMARY KEY ([file_id])
);
GO
ALTER TABLE [file_pending_update] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
GO

View File

@@ -1,15 +0,0 @@
-- Table: file_primitive_definition_link
CREATE TABLE [file_primitive_definition_link] (
[primitive_definition_id] int NOT NULL,
[file_id] int NOT NULL,
[is_needed_for_package] bit NOT NULL DEFAULT ((0)),
[is_needed_for_runtime] bit NOT NULL DEFAULT ((0)),
[is_needed_for_editor] bit NOT NULL DEFAULT ((0)),
[is_needed_for_browser] bit NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_file_primitive_definition_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id])
);
GO
ALTER TABLE [file_primitive_definition_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
GO

View File

@@ -1,13 +0,0 @@
-- Table: file_table
CREATE TABLE [file_table] (
[file_id] int NOT NULL,
[file_name] nvarchar(256) NOT NULL,
[vendor_name] nvarchar(256) NOT NULL,
[registration_type] int NOT NULL,
[subfolder] nvarchar(256) NOT NULL DEFAULT (''),
[file_version] nvarchar(50) NOT NULL DEFAULT (''),
[file_modified_time] nvarchar(50) NOT NULL DEFAULT (''),
CONSTRAINT [PK_file_table] PRIMARY KEY ([file_id])
);
GO

View File

@@ -1,14 +0,0 @@
-- Table: folder
CREATE TABLE [folder] (
[folder_id] int NOT NULL,
[folder_type] smallint NOT NULL,
[folder_name] nvarchar(64) NOT NULL,
[parent_folder_id] int NOT NULL,
[depth] int NOT NULL,
[has_objects] bit NOT NULL,
[has_folders] bit NOT NULL,
[timestamp_of_last_change] timestamp NOT NULL,
CONSTRAINT [PK_folder] PRIMARY KEY ([folder_id])
);
GO

View File

@@ -1,13 +0,0 @@
-- Table: folder_gobject_link
CREATE TABLE [folder_gobject_link] (
[folder_id] int NOT NULL,
[folder_type] smallint NOT NULL,
[gobject_id] int NOT NULL,
[timestamp_of_last_change] timestamp NOT NULL,
CONSTRAINT [PK_folder_gobject_link] PRIMARY KEY ([folder_id], [gobject_id], [gobject_id])
);
GO
ALTER TABLE [folder_gobject_link] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,18 +0,0 @@
-- Table: galaxy
CREATE TABLE [galaxy] (
[time_of_last_deploy] datetime NULL DEFAULT (getdate()),
[time_of_last_config_change] datetime NULL DEFAULT (getdate()),
[is_galaxy_installed] bit NOT NULL DEFAULT ((1)),
[time_of_last_reference_binding] datetime NULL DEFAULT (getdate()),
[timestamp_of_last_cascade] bigint NOT NULL DEFAULT ((1)),
[timestamp_of_last_visual_element_reference_bind] bigint NOT NULL DEFAULT ((0)),
[max_proxy_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)),
[max_visual_element_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)),
[is_migration_in_progress] bit NOT NULL DEFAULT ((0)),
[time_of_last_association_change] datetime NULL DEFAULT (getdate()),
[subscription_id] uniqueidentifier NULL,
[batch_id] uniqueidentifier NULL,
[iteration_id] int NOT NULL DEFAULT ((0))
);
GO

View File

@@ -1,7 +0,0 @@
-- Table: galaxy_data
CREATE TABLE [galaxy_data] (
[data_type] nvarchar(256) NOT NULL,
[data] image(2147483647) NULL
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: galaxy_settings
CREATE TABLE [galaxy_settings] (
[galaxyid] int NULL,
[default_qs_data] ntext(1073741823) NOT NULL,
[current_qs_data] ntext(1073741823) NOT NULL
);
GO

View File

@@ -1,35 +0,0 @@
-- Table: gobject
CREATE TABLE [gobject] (
[gobject_id] int NOT NULL,
[template_definition_id] int NOT NULL,
[derived_from_gobject_id] int NOT NULL DEFAULT ((0)),
[contained_by_gobject_id] int NOT NULL DEFAULT ((0)),
[area_gobject_id] int NOT NULL DEFAULT ((0)),
[hosted_by_gobject_id] int NOT NULL DEFAULT ((0)),
[checked_out_by_user_guid] uniqueidentifier NULL,
[default_symbol_gobject_id] int NOT NULL DEFAULT ((0)),
[default_display_gobject_id] int NOT NULL DEFAULT ((0)),
[checked_in_package_id] int NOT NULL DEFAULT ((0)),
[checked_out_package_id] int NOT NULL DEFAULT ((0)),
[deployed_package_id] int NOT NULL DEFAULT ((0)),
[last_deployed_package_id] int NOT NULL DEFAULT ((0)),
[tag_name] nvarchar(329) NOT NULL,
[contained_name] nvarchar(32) NOT NULL DEFAULT (''),
[identity_guid] uniqueidentifier NOT NULL DEFAULT (newid()),
[configuration_guid] uniqueidentifier NOT NULL,
[configuration_version] int NOT NULL,
[deployed_version] int NOT NULL DEFAULT ((0)),
[is_template] bit NOT NULL DEFAULT ((0)),
[is_hidden] bit NOT NULL DEFAULT ((0)),
[software_upgrade_needed] bit NOT NULL DEFAULT ((0)),
[hosting_tree_level] smallint NOT NULL DEFAULT ((0)),
[hierarchical_name] nvarchar(329) NOT NULL DEFAULT (''),
[namespace_id] smallint NOT NULL DEFAULT ((1)),
[deployment_pending_status] bit NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_gobject] PRIMARY KEY ([gobject_id], [namespace_id], [template_definition_id])
);
GO
ALTER TABLE [gobject] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: gobject_asset_order
CREATE TABLE [gobject_asset_order] (
[gobject_id] int NOT NULL,
[relative_index] float(53,) NOT NULL,
CONSTRAINT [PK_gobject_asset_order] PRIMARY KEY ([gobject_id])
);
GO
ALTER TABLE [gobject_asset_order] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,16 +0,0 @@
-- Table: gobject_change_log
CREATE TABLE [gobject_change_log] (
[gobject_change_log_id] int NOT NULL,
[gobject_id] int NOT NULL,
[change_date] datetime NULL,
[operation_id] smallint NOT NULL,
[user_comment] nvarchar(1024) NOT NULL DEFAULT (''),
[configuration_version] int NOT NULL DEFAULT ((0)),
[user_profile_name] nvarchar(256) NOT NULL,
CONSTRAINT [PK_gobject_change_log] PRIMARY KEY ([gobject_id], [operation_id])
);
GO
ALTER TABLE [gobject_change_log] ADD FOREIGN KEY ([operation_id]) REFERENCES [lookup_operation] ([operation_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: gobject_filter_info_timestamp
CREATE TABLE [gobject_filter_info_timestamp] (
[gobject_id] int NULL,
[timestamp_of_last_change] timestamp NOT NULL,
CONSTRAINT [PK_gobject_filter_info_timestamp] PRIMARY KEY ([timestamp_of_last_change], [gobject_id])
);
GO
ALTER TABLE [gobject_filter_info_timestamp] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,11 +0,0 @@
-- Table: gobject_friendly_name
CREATE TABLE [gobject_friendly_name] (
[gobject_id] int NOT NULL,
[friendly_name] nvarchar(1024) NOT NULL DEFAULT (''),
CONSTRAINT [PK_gobject_friendly_name] PRIMARY KEY ([gobject_id])
);
GO
ALTER TABLE [gobject_friendly_name] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,7 +0,0 @@
-- Table: gobject_log_details
CREATE TABLE [gobject_log_details] (
[gobject_id] int NOT NULL,
[tag_name] nvarchar(329) NOT NULL
);
GO

View File

@@ -1,6 +0,0 @@
-- Table: gobject_protected
CREATE TABLE [gobject_protected] (
[gobject_id] int NOT NULL
);
GO

View File

@@ -1,13 +0,0 @@
-- Table: instance
CREATE TABLE [instance] (
[gobject_id] int NOT NULL,
[mx_platform_id] smallint NOT NULL DEFAULT ((0)),
[mx_engine_id] smallint NOT NULL DEFAULT ((0)),
[mx_object_id] smallint NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_instance] PRIMARY KEY ([gobject_id], [gobject_id])
);
GO
ALTER TABLE [instance] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
GO

View File

@@ -1,6 +0,0 @@
-- Table: intouchviewapptemplate_allsymbols
CREATE TABLE [intouchviewapptemplate_allsymbols] (
[gobject_id] int NOT NULL
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: lookup_category
CREATE TABLE [lookup_category] (
[category_id] smallint NOT NULL,
[category_name] nvarchar(50) NOT NULL,
CONSTRAINT [PK_lookup_category] PRIMARY KEY ([category_id])
);
GO

View File

@@ -1,7 +0,0 @@
-- Table: lookup_folder
CREATE TABLE [lookup_folder] (
[folder_type] smallint NOT NULL,
[folder_type_name] nvarchar(32) NULL
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: lookup_operation
CREATE TABLE [lookup_operation] (
[operation_id] smallint NOT NULL,
[operation_code] nvarchar(50) NOT NULL,
[operation_name] nvarchar(256) NOT NULL,
CONSTRAINT [PK_lookup_operation] PRIMARY KEY ([operation_id])
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: lookup_package_op_status
CREATE TABLE [lookup_package_op_status] (
[status_id] int NOT NULL,
[status_name] nvarchar(50) NOT NULL,
CONSTRAINT [PK_lookup_package_op_status] PRIMARY KEY ([status_id])
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: lookup_status
CREATE TABLE [lookup_status] (
[status_id] int NOT NULL,
[status_name] nvarchar(50) NOT NULL,
CONSTRAINT [PK_lookup_status] PRIMARY KEY ([status_id])
);
GO

View File

@@ -1,7 +0,0 @@
-- Table: lookup_table_name
CREATE TABLE [lookup_table_name] (
[table_id] smallint NOT NULL,
[table_name] nvarchar(250) NULL
);
GO

View File

@@ -1,8 +0,0 @@
-- Table: namespace
CREATE TABLE [namespace] (
[namespace_id] smallint NOT NULL,
[namespace_name] nvarchar(32) NULL,
CONSTRAINT [PK_namespace] PRIMARY KEY ([namespace_id])
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: object_device_linkage
CREATE TABLE [object_device_linkage] (
[gobject_id] int NOT NULL,
[dio_id] int NOT NULL,
[sg_mx_primitive_id] smallint NOT NULL,
CONSTRAINT [PK_object_device_linkage] PRIMARY KEY ([gobject_id])
);
GO

View File

@@ -1,9 +0,0 @@
-- Table: object_wizard_overview_symbols
CREATE TABLE [object_wizard_overview_symbols] (
[gobject_id] int NOT NULL,
[visual_element_id] int NOT NULL,
[change_type] int NOT NULL,
[mx_primitive_id] int NULL
);
GO

View File

@@ -1,12 +0,0 @@
-- Table: object_wizard_symbol_override
CREATE TABLE [object_wizard_symbol_override] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[symbol_overrides] image(2147483647) NULL,
CONSTRAINT [PK_object_wizard_symbol_override] PRIMARY KEY ([gobject_id], [package_id], [gobject_id], [package_id])
);
GO
ALTER TABLE [object_wizard_symbol_override] ADD FOREIGN KEY ([package_id]) REFERENCES [package] ([package_id]);
GO

View File

@@ -1,15 +0,0 @@
-- Table: object_wizard_symbol_override_mapping
CREATE TABLE [object_wizard_symbol_override_mapping] (
[gobject_id] int NOT NULL,
[package_id] int NOT NULL,
[mx_primitive_id] smallint NOT NULL,
[has_local_overrides] bit NOT NULL,
[thumbnail] image(2147483647) NULL,
[preview] image(2147483647) NULL,
CONSTRAINT [PK_object_wizard_symbol_override_mapping] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id])
);
GO
ALTER TABLE [object_wizard_symbol_override_mapping] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]);
GO

View File

@@ -1,9 +0,0 @@
-- Table: old_checked_in_packages
CREATE TABLE [old_checked_in_packages] (
[package_id] int NULL,
[gobject_id] int NULL,
[is_template] bit NULL,
[is_being_referenced] bit NULL DEFAULT ((0))
);
GO

View File

@@ -1,13 +0,0 @@
-- Table: operation
CREATE TABLE [operation] (
[operation_id] int NOT NULL,
[user_profile_id] int NOT NULL,
[operation_name] nvarchar(300) NOT NULL,
[start_time] datetime NOT NULL DEFAULT (getdate()),
CONSTRAINT [PK_operation] PRIMARY KEY ([operation_id], [user_profile_id])
);
GO
ALTER TABLE [operation] ADD FOREIGN KEY ([user_profile_id]) REFERENCES [user_profile] ([user_profile_id]);
GO

View File

@@ -1,13 +0,0 @@
-- Table: operation_message
CREATE TABLE [operation_message] (
[message_id] int NOT NULL,
[operation_id] int NOT NULL,
[message_text] nvarchar(300) NOT NULL,
[message_time] datetime NOT NULL DEFAULT (getdate()),
CONSTRAINT [PK_operation_message] PRIMARY KEY ([message_id], [operation_id])
);
GO
ALTER TABLE [operation_message] ADD FOREIGN KEY ([operation_id]) REFERENCES [operation] ([operation_id]);
GO

Some files were not shown because too many files have changed in this diff Show More