Restructure the driver-facing docs to match the OtOpcUa v2 multi-driver
reality (Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, OPC UA Client
— 8 drivers total; Galaxy ships as three projects) and the capability-interface
architecture where every driver opts into IDriver + whichever of IReadable /
IWritable / ITagDiscovery / ISubscribable / IHostConnectivityProbe /
IPerCallHostResolver / IAlarmSource / IHistoryProvider / IRediscoverable it
supports. Doc scope follows the code: one-driver-specific docs scoped to that
driver, cross-driver concerns live once at the top level, per-driver specs
cross-link to docs/v2/driver-specs.md rather than duplicate.
What changed per file:
- docs/MxAccessBridge.md -> docs/drivers/Galaxy.md (git mv + rewrite): retitled
"Galaxy Driver", reframed as one of seven drivers. Added Project Split table
(Shared .NET Standard 2.0 / Host .NET 4.8 x86 / Proxy .NET 10) and Why
Out-of-Process section citing both the MXAccess bitness constraint and Tier C
stability isolation per docs/v2/plan.md section 4. Added IPC Transport
section covering pipe naming, MessagePack framing, DACL that denies Admins,
shared-secret handshake, heartbeat, and CallAsync<TReq,TResp> dispatch.
Moved file paths from src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/* to
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccess/* and added the
Shared + Proxy key-file tables. Added CapabilityInvoker + OTOPCUA0001
analyzer callout. Cross-linked to drivers/README.md, Galaxy-Repository.md,
HistoricalDataAccess.md.
- docs/GalaxyRepository.md -> docs/drivers/Galaxy-Repository.md (git mv +
rewrite): retitled "Galaxy Repository — Tag Discovery for the Galaxy
Driver", opened with a comparison table showing how every driver's
ITagDiscovery source is different (AB CIP @tags walker, TwinCAT
SymbolLoaderFactory, FOCAS CNC queries, OPC UA Client Session.Browse, etc).
Repositioned GalaxyRepositoryService as the Galaxy driver's
ITagDiscovery.DiscoverAsync implementation. Updated paths to
Driver.Galaxy.Host/Backend/GalaxyRepository/*. Added IRediscoverable section
covering the on-change-redeploy IPC path.
- docs/drivers/README.md (new): index with ground-truth driver table —
project path, stability tier, wire library, capability-interface list, and
one notable quirk per driver. Verified against the driver csproj files and
class declarations on focas-pr3-remaining-capabilities (the most recent
branch containing every driver). Galaxy gets its own dedicated docs; the
other seven drivers cross-link to docs/v2/driver-specs.md. Lists the full
Core.Abstractions capability surface, DriverTypeRegistry, CapabilityInvoker,
and OTOPCUA0001 analyzer.
- docs/HistoricalDataAccess.md (rewrite): reframed around IHistoryProvider as
a per-driver optional capability interface. Replaced v1 HistorianPluginLoader
/ AvevaHistorianPluginEntry plugin architecture with the v2 story —
Historian.Aveva was merged into Driver.Galaxy.Host/Backend/Historian/ and
IPC-forwarded through GalaxyProxyDriver. Documented all four IHistoryProvider
methods (ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync /
ReadEventsAsync), CapabilityInvoker wrapping with DriverCapability.HistoryRead,
and the per-driver coverage matrix (Galaxy + OPC UA Client implement; the
six protocol drivers don't and return BadHistoryOperationUnsupported). Kept
the cluster-failover + health-counter + quality-mapping detail for the
Galaxy Historian implementation. Flagged one gap: Proxy forwards all four
history message kinds but the Host-side HistoryAggregateType -> AnalogSummary
column mapping may surface GalaxyIpcException{Code="not-implemented"} on a
given branch until the Phase 2 Galaxy out-of-process gate lands.
Driver list built against ground truth (src on focas-pr3-remaining-capabilities):
Driver.Galaxy.{Shared,Host,Proxy}, Driver.Modbus, Driver.S7, Driver.AbCip,
Driver.AbLegacy, Driver.TwinCAT, Driver.FOCAS, Driver.OpcUaClient.
Capability interface lists verified against each *Driver.cs class declaration.
Aveva Historian ported to Driver.Galaxy.Host/Backend/Historian/; no separate
Historian.Aveva assembly on v2 branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Galaxy Repository — Tag Discovery for the Galaxy Driver
GalaxyRepositoryService reads the Galaxy object hierarchy and attribute metadata from the System Platform Galaxy Repository SQL Server database. It is the Galaxy driver's implementation of ITagDiscovery.DiscoverAsync — every driver has its own discovery source, and the Galaxy driver's is a direct SQL query against the Galaxy Repository (the ZB database). Other drivers use completely different mechanisms:
| Driver | ITagDiscovery source |
|---|---|
| Galaxy | ZB SQL hierarchy + attribute queries (this doc) |
| AB CIP | @tags walker against the PLC controller |
| AB Legacy | Data-table scan via PCCC LogicalRead on the PLC |
| TwinCAT | Beckhoff SymbolLoaderFactory — uploads the full symbol tree from the ADS runtime |
| S7 | Config-DB enumeration (no native symbol upload for S7comm) |
| Modbus | Config-DB enumeration (flat register map, user-authored) |
| FOCAS | CNC queries (cnc_rdaxisname, cnc_rdmacroinfo, …) + optional Config-DB overlays |
| OPC UA Client | Session.Browse against the remote server |
GalaxyRepositoryService lives in src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/GalaxyRepository/ — Host-side, .NET Framework 4.8 x86, same process that owns the MXAccess COM objects. The Proxy forwards discovery over IPC the same way it forwards reads and writes.
Connection Configuration
GalaxyRepositoryConfiguration controls database access:
| Property | Default | Description |
|---|---|---|
ConnectionString |
Server=localhost;Database=ZB;Integrated Security=true; |
SQL Server connection using Windows Authentication |
ChangeDetectionIntervalSeconds |
30 |
Polling frequency for deploy change detection |
CommandTimeoutSeconds |
30 |
SQL command timeout for all queries |
ExtendedAttributes |
false |
When true, loads primitive-level attributes in addition to dynamic attributes |
Scope |
Galaxy |
Galaxy loads all deployed objects. LocalPlatform filters to the local platform's subtree only |
PlatformName |
null |
Explicit platform hostname for LocalPlatform filtering. When null, uses Environment.MachineName |
The connection uses Windows Authentication because the Galaxy Repository database is local to the System Platform node and secured through domain credentials.
SQL Queries
All queries are embedded as const string fields in GalaxyRepositoryService. No dynamic SQL is used. Project convention GR-006 requires const string SQL queries; any new query must be added as a named constant rather than built at runtime.
Hierarchy query
Returns deployed Galaxy objects with their parent relationships, browse names, and template derivation chains:
- Joins
gobjecttotemplate_definitionto filter by relevantcategory_idvalues (1, 3, 4, 10, 11, 13, 17, 24, 26) - Uses
contained_nameas the browse name, falling back totag_namewhencontained_nameis null or empty - Resolves the parent using
contained_by_gobject_idwhen non-zero, otherwise falls back toarea_gobject_id - Marks objects with
category_id = 13as areas - Filters to
is_template = 0(instances only, not templates) - Filters to
deployed_package_id <> 0(deployed objects only) - Returns a
template_chaincolumn built by a recursive CTE that walksgobject.derived_from_gobject_idfrom each instance through its immediate template and ancestor templates (depth guard< 10). Template names are ordered by depth and joined with|viaSTUFF(... FOR XML PATH('')). Example:TestMachine_001returns$TestMachine|$gMachine|$gUserDefined|$UserDefined. The C# repository reader splits the column on|, trims, and populatesGalaxyObjectInfo.TemplateChain, which is consumed byAlarmObjectFilterfor template-based alarm filtering. See Alarm Tracking. - Returns
template_definition.category_idas acategory_idcolumn, populated intoGalaxyObjectInfo.CategoryId. The runtime status probe manager filters this down toCategoryId == 1($WinPlatform) andCategoryId == 3($AppEngine) to decide which objects get a<Host>.ScanStateprobe advised. Also used during the hosted-variables walk to identify Platform/Engine ancestors. - Returns
gobject.hosted_by_gobject_idas ahosted_by_gobject_idcolumn, populated intoGalaxyObjectInfo.HostedByGobjectId. This is the runtime host of the object (e.g., which$AppEngineactually runs it), not the browse-containment parent (contained_by_gobject_id). The two are often different — an object can live in one Area in the browse tree but be hosted by an Engine on a different Platform for runtime execution. The driver walks this chain duringBuildHostedVariablesMapto find the nearest$WinPlatformor$AppEngineancestor so subtree quality invalidation on a Stopped host reaches exactly the variables that were actually executing there. Note: the Galaxy schema column is namedhosted_by_gobject_id(nothost_gobject_idas some documentation sources guess). See Galaxy driver — Per-Host Runtime Status Probes.
Attributes query (standard)
Returns user-defined dynamic attributes for deployed objects:
- Uses a recursive CTE (
deployed_package_chain) to walk the package inheritance chain fromdeployed_package_idthroughderived_from_package_id, limited to 10 levels - Joins
dynamic_attributeon each package in the chain to collect inherited attributes - Uses
ROW_NUMBER() OVER (PARTITION BY gobject_id, attribute_name ORDER BY depth)to pick the most-derived definition when an attribute is overridden at multiple levels - Builds
full_tag_referenceastag_name.attribute_namewith[]appended for arrays - Extracts
array_dimensionfrom the binarymx_valuecolumn (bytes 13-16, little-endian int32) - Detects historized attributes by checking for a
HistoryExtensionprimitive instance - Detects alarm attributes by checking for an
AlarmExtensionprimitive instance - Excludes internal attributes (names starting with
_) and.Descriptionsuffixes - Filters by
mx_attribute_categoryto include only user-relevant categories
Attributes query (extended)
When ExtendedAttributes = true, a more comprehensive query runs that unions two sources:
- Primitive attributes — Joins through
primitive_instanceandattribute_definitionto include system-level attributes from primitive components. Each attribute carries itsprimitive_nameso the address space can group them under their parent variable. - Dynamic attributes — The same CTE-based query as the standard path, with an empty
primitive_name.
The full_tag_reference for primitive attributes follows the pattern tag_name.primitive_name.attribute_name (e.g., TestMachine_001.AlarmAttr.InAlarm).
Change detection query
A single-column query: SELECT time_of_last_deploy FROM galaxy. The galaxy table contains one row with the timestamp of the most recent deployment.
Why deployed_package_id Instead of checked_in_package_id
The Galaxy maintains two package references for each object:
checked_in_package_id— the latest saved version, which may include undeployed configuration changesdeployed_package_id— the version currently running on the target platform
The queries filter on deployed_package_id <> 0 because the OPC UA address space must mirror what is actually running in the Galaxy runtime. Using checked_in_package_id would expose attributes and objects that exist in the IDE but have not been deployed, causing mismatches between the OPC UA address space and the MXAccess runtime.
Platform Scope Filter
When Scope is set to LocalPlatform, the repository applies a post-query C# filter to restrict the address space to objects hosted by the local platform. This reduces memory footprint, MXAccess subscription count, and address space size on multi-node Galaxy deployments where each OPC UA server instance only needs to serve its own platform's objects.
How it works
- Platform lookup — A separate
const stringSQL query (PlatformLookupSql) readsplatform_gobject_idandnode_namefrom theplatformtable for all deployed platforms. This runs once per hierarchy load. - Platform matching — The configured
PlatformName(orEnvironment.MachineNamewhen null) is matched case-insensitively against thenode_namecolumn. If no match is found, a warning is logged listing the available platforms and the address space is empty. - Host chain collection — The filter collects the matching platform's
gobject_id, then iterates the hierarchy to find all$AppEngine(category 3) objects whoseHostedByGobjectIdequals the platform. This produces the full set of host gobject_ids under the local platform. - Object inclusion — All non-area objects whose
HostedByGobjectIdis in the host set are included, along with the hosts themselves. - Area retention —
ParentGobjectIdchains are walked upward from included objects to pull in ancestor areas, keeping the browse tree connected. Areas that contain no local descendants are excluded. - Attribute filtering — The set of included
gobject_idvalues is cached afterGetHierarchyAsyncand reused byGetAttributesAsyncto filter attributes to the same scope.
Design rationale
The filter is applied in C# rather than SQL because project convention GR-006 requires const string SQL queries with no dynamic SQL. The hierarchy query already returns HostedByGobjectId and CategoryId on every row, so all information needed for filtering is already in memory after the query runs. The only new SQL is the lightweight platform lookup query.
Configuration
"GalaxyRepository": {
"Scope": "LocalPlatform",
"PlatformName": null
}
- Set
Scopeto"LocalPlatform"to enable filtering. Default is"Galaxy"(load everything). - Set
PlatformNameto an explicit hostname to target a specific platform, or leave null to use the local machine name.
Startup log
When LocalPlatform is active, the startup log shows the filtering result:
GalaxyRepository.Scope="LocalPlatform", PlatformName=MYNODE
GetHierarchyAsync returned 49 objects
GetPlatformsAsync returned 2 platform(s)
Scope filter targeting platform 'MYNODE' (gobject_id=1042)
Scope filter retained 25 of 49 objects for platform 'MYNODE'
GetAttributesAsync returned 4206 attributes (extended=true)
Scope filter retained 2100 of 4206 attributes
Change Detection Polling and IRediscoverable
ChangeDetectionService runs a background polling loop in the Host process that calls GetLastDeployTimeAsync at the configured interval. It compares the returned timestamp against the last known value:
- On the first poll (no previous state), the timestamp is recorded and
OnGalaxyChangedfires unconditionally - On subsequent polls,
OnGalaxyChangedfires only whentime_of_last_deploydiffers from the cached value
When the event fires, the Host re-runs the hierarchy and attribute queries and pushes the result back to the Server via an IPC RediscoveryNeeded message. That surfaces on GalaxyProxyDriver as the IRediscoverable.OnRediscoveryNeeded event; the Server's DriverNodeManager consumes it and calls SyncAddressSpace to compute the diff against the live address space.
The polling approach is used because the Galaxy Repository database does not provide change notifications. The galaxy.time_of_last_deploy column updates only on completed deployments, so the polling interval controls how quickly the OPC UA address space reflects Galaxy changes.
TestConnection
TestConnectionAsync runs SELECT 1 against the configured database. This is used at Host startup to verify connectivity before attempting the full hierarchy query.
Key source files
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/GalaxyRepository/GalaxyRepositoryService.cs— SQL queries and data accesssrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/GalaxyRepository/PlatformScopeFilter.cs— Platform-based hierarchy and attribute filteringsrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/GalaxyRepository/ChangeDetectionService.cs— Deploy timestamp polling loopsrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Configuration/GalaxyRepositoryConfiguration.cs— Connection, polling, and scope settingssrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Domain/PlatformInfo.cs— Platform-to-hostname DTOsrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/DiscoveryResponse.cs— IPC DTO the Host uses to return hierarchy + attribute results across the pipe