Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64e3fbe035 | |||
| f9fc7dd2e1 | |||
| 7dfbca6469 | |||
| 44b8a9c7ff | |||
| 60beb9128e | |||
| 6884de9774 | |||
| c064ec16cf | |||
| ed1c17bc7b | |||
| 1e64488c0d |
+12
-12
@@ -9,8 +9,9 @@ Mac-friendly multi-cluster OtOpcUa fleet for manual UI exercise + integration sm
|
||||
| Service | Role | Ports |
|
||||
|---|---|---|
|
||||
| `sql` | SQL Server 2022 — single `OtOpcUa` ConfigDb shared by all three clusters | host `14330` → container `1433` |
|
||||
| `ldap` | OpenLDAP with dev users `alice` / `bob` | host `3893` → container `1389` |
|
||||
| `traefik` | Routes :80 by Host header / PathPrefix | host `80`, dashboard `8080` |
|
||||
| `traefik` | Routes :80 by Host header / PathPrefix | host `80`, dashboard `8089` |
|
||||
|
||||
Authentication runs in `DevStubMode` — every host container has `Authentication__Ldap__DevStubMode=true` set, so the LDAP service is not part of the dev compose right now (the `bitnami/openldap:2.6` image was retired and the legacy tag crashes mid-setup with exit 68). Any non-empty username/password signs in as `FleetAdmin`. To restore a real LDAP service, drop the env var and add an `openldap`-compatible image back to compose.
|
||||
|
||||
### Main cluster — split admin/driver roles
|
||||
|
||||
@@ -59,6 +60,12 @@ The SQL lives at `seed/seed-clusters.sql`; the wait-and-apply wrapper lives at `
|
||||
docker compose -f docker-dev/docker-compose.yml run --rm cluster-seed
|
||||
```
|
||||
|
||||
### Galaxy / MxAccess gateway
|
||||
|
||||
The seed also pre-creates a `SystemPlatform` Namespace + a `GalaxyMxGateway` DriverInstance in the MAIN cluster pointing at `http://10.100.0.48:5120`. The API key is resolved from the `GALAXY_MXGW_API_KEY` env var set on every driver-role container in compose; override via `GALAXY_MXGW_API_KEY=... docker compose up -d` to swap keys without editing the compose file.
|
||||
|
||||
The DriverHost actor doesn't spawn drivers from raw DriverInstance rows on its own — the v2 deploy lifecycle requires a *sealed Deployment* before drivers materialise. After first bring-up, sign in to the Admin UI and click **Deploy current configuration** on `/deployments` to compose the seeded rows into an artifact and dispatch it. The Galaxy driver instance will start its gRPC connection to the gateway on the next deploy ack.
|
||||
|
||||
## Bring up
|
||||
|
||||
```bash
|
||||
@@ -70,7 +77,7 @@ docker compose -f docker-dev/docker-compose.yml up -d --build
|
||||
open http://localhost # main cluster admin UI
|
||||
open http://site-a.localhost # site A admin UI
|
||||
open http://site-b.localhost # site B admin UI
|
||||
open http://localhost:8080 # Traefik dashboard
|
||||
open http://localhost:8089 # Traefik dashboard
|
||||
```
|
||||
|
||||
On macOS, `*.localhost` resolves to `127.0.0.1` automatically. On Linux add `127.0.0.1 site-a.localhost site-b.localhost` to `/etc/hosts` if your resolver doesn't.
|
||||
@@ -79,14 +86,7 @@ The first build takes a few minutes (.NET SDK image + restore + publish). Subseq
|
||||
|
||||
## Auth (dev only)
|
||||
|
||||
Use one of the LDAP dev users from `LDAP_USERS` in `docker-compose.yml`:
|
||||
|
||||
| Username | Password |
|
||||
|---|---|
|
||||
| `alice` | `alice123` |
|
||||
| `bob` | `bob123` |
|
||||
|
||||
The compose mounts everyone into `ou=FleetAdmin` so the dev role mapping resolves to `FleetAdmin`.
|
||||
`Authentication__Ldap__DevStubMode=true` is set on every host container, so any non-empty username/password signs in as a `FleetAdmin` user without contacting an LDAP server. **Do not** ship this configuration to production — set `DevStubMode=false` and wire a real LDAP backend before any non-dev deployment.
|
||||
|
||||
## Tear down
|
||||
|
||||
@@ -98,7 +98,7 @@ The `-v` drops the SQL + LDAP volumes; remove it to keep ConfigDb state across r
|
||||
|
||||
## Failover smoke
|
||||
|
||||
1. Watch the Traefik dashboard at `http://localhost:8080`. Both `admin-a` and `admin-b` should be listed as healthy in the `otopcua-admin` service.
|
||||
1. Watch the Traefik dashboard at `http://localhost:8089`. Both `admin-a` and `admin-b` should be listed as healthy in the `otopcua-admin` service.
|
||||
2. `docker compose -f docker-dev/docker-compose.yml stop admin-a` — `admin-b` should pick up the admin role-leader within ~15 s (Akka split-brain stable-after). Traefik will route traffic to `admin-b` once its `/health/active` returns 200.
|
||||
3. `docker compose -f docker-dev/docker-compose.yml start admin-a` — `admin-a` rejoins as a follower; `admin-b` keeps the leader role until something disturbs it.
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
# open http://localhost # main cluster Blazor admin UI
|
||||
# open http://site-a.localhost # site A admin UI
|
||||
# open http://site-b.localhost # site B admin UI
|
||||
# open http://localhost:8080 # Traefik dashboard
|
||||
# open http://localhost:8089 # Traefik dashboard (8080 is the sister scadalink stack)
|
||||
#
|
||||
# Tear-down: docker compose -f docker-dev/docker-compose.yml down -v
|
||||
|
||||
@@ -71,17 +71,12 @@ services:
|
||||
entrypoint: ["/bin/bash", "/seed/entrypoint.sh"]
|
||||
restart: "no"
|
||||
|
||||
ldap:
|
||||
image: bitnami/openldap:2.6
|
||||
environment:
|
||||
LDAP_ROOT: "dc=lmxopcua,dc=local"
|
||||
LDAP_ADMIN_USERNAME: "admin"
|
||||
LDAP_ADMIN_PASSWORD: "ldapadmin"
|
||||
LDAP_USERS: "alice,bob"
|
||||
LDAP_PASSWORDS: "alice123,bob123"
|
||||
LDAP_USER_DC: "ou=FleetAdmin"
|
||||
ports:
|
||||
- "3893:1389"
|
||||
# OpenLDAP was previously here but the bitnami/openldap:2.6 image was retired
|
||||
# (manifest gone) and bitnamilegacy/openldap:2.6 crashes during LDIF setup with
|
||||
# exit 68. For the dev compose every host container now runs with
|
||||
# Authentication__Ldap__DevStubMode=true, so any non-empty username/password
|
||||
# signs in as `FleetAdmin`. Restore a real LDAP service when there's a need
|
||||
# for end-to-end LDAP coverage (the host code path is unchanged).
|
||||
|
||||
admin-a: &otopcua-host
|
||||
build:
|
||||
@@ -102,9 +97,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
|
||||
admin-b:
|
||||
<<: *otopcua-host
|
||||
@@ -120,9 +114,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
|
||||
driver-a:
|
||||
<<: *otopcua-host
|
||||
@@ -134,6 +127,9 @@ services:
|
||||
Cluster__PublicHostname: "driver-a"
|
||||
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
||||
Cluster__Roles__0: "driver"
|
||||
# Resolved at runtime by GalaxyDriver.ResolveApiKey when a DriverInstance's
|
||||
# Gateway.ApiKeySecretRef = "env:GALAXY_MXGW_API_KEY".
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4840:4840"
|
||||
|
||||
@@ -147,6 +143,7 @@ services:
|
||||
Cluster__PublicHostname: "driver-b"
|
||||
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
||||
Cluster__Roles__0: "driver"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4841:4840"
|
||||
|
||||
@@ -170,9 +167,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4842:4840"
|
||||
|
||||
@@ -194,9 +190,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4843:4840"
|
||||
|
||||
@@ -217,9 +212,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4844:4840"
|
||||
|
||||
@@ -241,9 +235,8 @@ services:
|
||||
Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
|
||||
Security__Jwt__Issuer: "otopcua-dev"
|
||||
Security__Jwt__Audience: "otopcua-dev"
|
||||
Authentication__Ldap__Server: "ldap"
|
||||
Authentication__Ldap__Port: "1389"
|
||||
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||
Authentication__Ldap__DevStubMode: "true"
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua__UY_NKlBl3vWuZt8HD7usfZsU76eibMKB6CufwzabUI}"
|
||||
ports:
|
||||
- "4845:4840"
|
||||
|
||||
@@ -256,7 +249,7 @@ services:
|
||||
- --api.insecure=true
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
- "8089:8080" # 8080 conflicts with the sister scadalink dev stack
|
||||
volumes:
|
||||
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||
depends_on:
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# docker-dev cluster-seed entrypoint. Waits for the host containers to finish
|
||||
# their EF Core auto-migration (which creates the ServerCluster table), then
|
||||
# applies the idempotent seed script.
|
||||
# docker-dev cluster-seed entrypoint. Waits for the OtOpcUa ConfigDb schema to
|
||||
# be in place, then applies the idempotent row seed.
|
||||
#
|
||||
# Image: mcr.microsoft.com/mssql-tools (Debian + sqlcmd at /opt/mssql-tools18/bin).
|
||||
# IMPORTANT: this container does NOT run EF migrations — sqlcmd can't execute
|
||||
# the V2 migration script cleanly because it contains CREATE PROCEDURE
|
||||
# statements inside IF NOT EXISTS BEGIN ... END blocks (procs must be the
|
||||
# first statement in their batch). Migrations are owned by the operator:
|
||||
#
|
||||
# dotnet ef database update \
|
||||
# --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration \
|
||||
# --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host
|
||||
#
|
||||
# (with ConnectionStrings__ConfigDb pointing at Server=localhost,14330;...).
|
||||
# Once the schema is in place, restart the cluster-seed container — or just
|
||||
# `docker compose up -d` and the seed will pick up where it left off thanks to
|
||||
# the IF NOT EXISTS guards in seed-clusters.sql.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SQLCMD="/opt/mssql-tools18/bin/sqlcmd"
|
||||
SQLCMD="/opt/mssql-tools/bin/sqlcmd"
|
||||
SERVER="${SQL_HOST:-sql},1433"
|
||||
USER="${SQL_USER:-sa}"
|
||||
PASS="${SQL_PASSWORD:-OtOpcUa!Dev123}"
|
||||
DB="${SQL_DATABASE:-OtOpcUa}"
|
||||
|
||||
run_sql() {
|
||||
"$SQLCMD" -S "$SERVER" -U "$USER" -P "$PASS" -d "$DB" -No -b -h -1 "$@"
|
||||
run_sql_in() {
|
||||
local target_db="$1"; shift
|
||||
# -I forces SET QUOTED_IDENTIFIER ON (needed for filtered indexes if you
|
||||
# ever extend this script to touch them).
|
||||
"$SQLCMD" -S "$SERVER" -U "$USER" -P "$PASS" -d "$target_db" -b -h -1 -I "$@"
|
||||
}
|
||||
|
||||
echo "[cluster-seed] waiting for SQL Server to accept connections..."
|
||||
until run_sql -Q "SELECT 1" >/dev/null 2>&1; do
|
||||
until run_sql_in master -Q "SELECT 1" >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
echo "[cluster-seed] SQL Server up."
|
||||
|
||||
echo "[cluster-seed] waiting for $DB.ServerCluster (host containers must finish EF migration)..."
|
||||
until run_sql -Q "IF OBJECT_ID('dbo.ServerCluster') IS NULL THROW 50001, 'missing', 1; SELECT 1" >/dev/null 2>&1; do
|
||||
echo "[cluster-seed] waiting for ${DB} database + dbo.ServerCluster table (operator must run dotnet ef database update)..."
|
||||
until run_sql_in "$DB" -Q "IF OBJECT_ID('dbo.ServerCluster') IS NULL THROW 50001, 'missing', 1; SELECT 1" >/dev/null 2>&1; do
|
||||
sleep 3
|
||||
done
|
||||
echo "[cluster-seed] schema ready."
|
||||
|
||||
echo "[cluster-seed] applying seed-clusters.sql..."
|
||||
run_sql -i /seed/seed-clusters.sql
|
||||
|
||||
echo "[cluster-seed] applying seed-clusters.sql (ServerCluster + ClusterNode rows)..."
|
||||
run_sql_in "$DB" -i /seed/seed-clusters.sql
|
||||
echo "[cluster-seed] done."
|
||||
|
||||
@@ -55,45 +55,130 @@ IF NOT EXISTS (SELECT 1 FROM dbo.ServerCluster WHERE ClusterId = 'SITE-B')
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- ClusterNode — main cluster OPC UA publishers
|
||||
--
|
||||
-- NodeId is "<compose-service>:4053" so it matches what ClusterRoleInfo +
|
||||
-- ConfigPublishCoordinator derive from Akka.Cluster.Get(system).State.Members
|
||||
-- (member.Address.Host:Port). NodeDeploymentState.NodeId is FK-bound to
|
||||
-- ClusterNode.NodeId; mismatched values cause FK 547 on deploy.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-a')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-a:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('driver-a', 'MAIN', 'driver-a', 4840, 8081, 'urn:OtOpcUa:driver-a', 200, 1, 'docker-dev-seed');
|
||||
VALUES ('driver-a:4053', 'MAIN', 'driver-a', 4840, 8081, 'urn:OtOpcUa:driver-a', 200, 1, 'docker-dev-seed');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-b')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-b:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('driver-b', 'MAIN', 'driver-b', 4840, 8081, 'urn:OtOpcUa:driver-b', 150, 1, 'docker-dev-seed');
|
||||
VALUES ('driver-b:4053', 'MAIN', 'driver-b', 4840, 8081, 'urn:OtOpcUa:driver-b', 150, 1, 'docker-dev-seed');
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- ClusterNode — site A
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-1')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-1:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('site-a-1', 'SITE-A', 'site-a-1', 4840, 8081, 'urn:OtOpcUa:site-a-1', 200, 1, 'docker-dev-seed');
|
||||
VALUES ('site-a-1:4053', 'SITE-A', 'site-a-1', 4840, 8081, 'urn:OtOpcUa:site-a-1', 200, 1, 'docker-dev-seed');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-2')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-2:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('site-a-2', 'SITE-A', 'site-a-2', 4840, 8081, 'urn:OtOpcUa:site-a-2', 150, 1, 'docker-dev-seed');
|
||||
VALUES ('site-a-2:4053', 'SITE-A', 'site-a-2', 4840, 8081, 'urn:OtOpcUa:site-a-2', 150, 1, 'docker-dev-seed');
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- ClusterNode — site B
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-1')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-1:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('site-b-1', 'SITE-B', 'site-b-1', 4840, 8081, 'urn:OtOpcUa:site-b-1', 200, 1, 'docker-dev-seed');
|
||||
VALUES ('site-b-1:4053', 'SITE-B', 'site-b-1', 4840, 8081, 'urn:OtOpcUa:site-b-1', 200, 1, 'docker-dev-seed');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-2')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-2:4053')
|
||||
INSERT INTO dbo.ClusterNode
|
||||
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES ('site-b-2', 'SITE-B', 'site-b-2', 4840, 8081, 'urn:OtOpcUa:site-b-2', 150, 1, 'docker-dev-seed');
|
||||
VALUES ('site-b-2:4053', 'SITE-B', 'site-b-2', 4840, 8081, 'urn:OtOpcUa:site-b-2', 150, 1, 'docker-dev-seed');
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Galaxy MxAccess gateway — MAIN cluster
|
||||
--
|
||||
-- Namespace.Kind=SystemPlatform is required for Galaxy/MXAccess data per
|
||||
-- decision #107; raw equipment drivers use Equipment. DriverInstance points
|
||||
-- at the external mxaccessgw process. The driver code lives in this repo
|
||||
-- (.NET 10, cross-platform); only the gateway worker needs Windows.
|
||||
--
|
||||
-- ApiKeySecretRef = env:GALAXY_MXGW_API_KEY → resolved at runtime by
|
||||
-- GalaxyDriver.ResolveApiKey. The env var is set on every driver-role
|
||||
-- container in docker-compose.yml.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Namespace WHERE NamespaceId = 'MAIN-galaxy')
|
||||
INSERT INTO dbo.Namespace
|
||||
(NamespaceRowId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled, Notes)
|
||||
VALUES
|
||||
(NEWID(), 'MAIN-galaxy', 'MAIN', 'SystemPlatform',
|
||||
'urn:zb:docker-dev:galaxy', 1,
|
||||
'docker-dev seed — Galaxy / MXAccess namespace served by the MAIN cluster.');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.DriverInstance WHERE DriverInstanceId = 'MAIN-galaxy-mxgw')
|
||||
INSERT INTO dbo.DriverInstance
|
||||
(DriverInstanceRowId, DriverInstanceId, ClusterId, NamespaceId, Name, DriverType, Enabled, DriverConfig)
|
||||
VALUES
|
||||
(NEWID(), 'MAIN-galaxy-mxgw', 'MAIN', 'MAIN-galaxy',
|
||||
'MxAccess gateway (10.100.0.48:5120)', 'GalaxyMxGateway', 1,
|
||||
N'{
|
||||
"Gateway": {
|
||||
"Endpoint": "http://10.100.0.48:5120",
|
||||
"ApiKeySecretRef": "env:GALAXY_MXGW_API_KEY",
|
||||
"UseTls": false,
|
||||
"ConnectTimeoutSeconds": 10,
|
||||
"DefaultCallTimeoutSeconds": 30
|
||||
},
|
||||
"MxAccess": {
|
||||
"ClientName": "OtOpcUa-MAIN-docker-dev",
|
||||
"PublishingIntervalMs": 1000
|
||||
},
|
||||
"Repository": {
|
||||
"DiscoverPageSize": 5000,
|
||||
"WatchDeployEvents": true
|
||||
},
|
||||
"Reconnect": {
|
||||
"InitialBackoffMs": 500,
|
||||
"MaxBackoffMs": 30000,
|
||||
"ReplayOnSessionLost": true
|
||||
}
|
||||
}');
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Galaxy test tags — TestMachine_001.TestAlarm001..003
|
||||
--
|
||||
-- SystemPlatform-namespace tags have EquipmentId=NULL and use FolderPath +
|
||||
-- Name to address the MXAccess item. The Galaxy driver subscribes via the
|
||||
-- "FolderPath.Name" MXAccess reference form; OPC UA browse path is the
|
||||
-- equivalent "FolderPath/Name" under the SystemPlatform namespace.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Tag WHERE TagId = 'MAIN-galaxy-TestMachine_001-TestAlarm001')
|
||||
INSERT INTO dbo.Tag
|
||||
(TagRowId, TagId, DriverInstanceId, DeviceId, EquipmentId, Name, FolderPath, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)
|
||||
VALUES
|
||||
(NEWID(), 'MAIN-galaxy-TestMachine_001-TestAlarm001', 'MAIN-galaxy-mxgw', NULL, NULL,
|
||||
'TestAlarm001', 'TestMachine_001', 'Boolean', 0, 0, NULL, N'{}');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Tag WHERE TagId = 'MAIN-galaxy-TestMachine_001-TestAlarm002')
|
||||
INSERT INTO dbo.Tag
|
||||
(TagRowId, TagId, DriverInstanceId, DeviceId, EquipmentId, Name, FolderPath, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)
|
||||
VALUES
|
||||
(NEWID(), 'MAIN-galaxy-TestMachine_001-TestAlarm002', 'MAIN-galaxy-mxgw', NULL, NULL,
|
||||
'TestAlarm002', 'TestMachine_001', 'Boolean', 0, 0, NULL, N'{}');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Tag WHERE TagId = 'MAIN-galaxy-TestMachine_001-TestAlarm003')
|
||||
INSERT INTO dbo.Tag
|
||||
(TagRowId, TagId, DriverInstanceId, DeviceId, EquipmentId, Name, FolderPath, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)
|
||||
VALUES
|
||||
(NEWID(), 'MAIN-galaxy-TestMachine_001-TestAlarm003', 'MAIN-galaxy-mxgw', NULL, NULL,
|
||||
'TestAlarm003', 'TestMachine_001', 'Boolean', 0, 0, NULL, N'{}');
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
@@ -104,3 +189,7 @@ COMMIT TRANSACTION;
|
||||
SELECT ClusterId, Name, NodeCount, RedundancyMode FROM dbo.ServerCluster ORDER BY ClusterId;
|
||||
SELECT NodeId, ClusterId, Host, OpcUaPort, ApplicationUri, ServiceLevelBase
|
||||
FROM dbo.ClusterNode ORDER BY ClusterId, NodeId;
|
||||
SELECT NamespaceId, ClusterId, Kind, NamespaceUri FROM dbo.Namespace ORDER BY ClusterId, NamespaceId;
|
||||
SELECT DriverInstanceId, ClusterId, DriverType, NamespaceId, Name
|
||||
FROM dbo.DriverInstance ORDER BY ClusterId, DriverInstanceId;
|
||||
SELECT TagId, DriverInstanceId, FolderPath, Name, DataType FROM dbo.Tag ORDER BY DriverInstanceId, FolderPath, Name;
|
||||
|
||||
@@ -28,6 +28,14 @@ http:
|
||||
services:
|
||||
otopcua-admin:
|
||||
loadBalancer:
|
||||
# Blazor Server uses SignalR; the WebSocket upgrade must hit the same
|
||||
# backend that owns the circuit ID. Sticky cookie keeps each session
|
||||
# pinned to one node so the post-handshake WebSocket doesn't 404.
|
||||
sticky:
|
||||
cookie:
|
||||
name: otopcua_lb
|
||||
httpOnly: true
|
||||
sameSite: lax
|
||||
servers:
|
||||
- url: "http://admin-a:9000"
|
||||
- url: "http://admin-b:9000"
|
||||
@@ -38,6 +46,14 @@ http:
|
||||
|
||||
otopcua-site-a:
|
||||
loadBalancer:
|
||||
# Blazor Server uses SignalR; the WebSocket upgrade must hit the same
|
||||
# backend that owns the circuit ID. Sticky cookie keeps each session
|
||||
# pinned to one node so the post-handshake WebSocket doesn't 404.
|
||||
sticky:
|
||||
cookie:
|
||||
name: otopcua_lb
|
||||
httpOnly: true
|
||||
sameSite: lax
|
||||
servers:
|
||||
- url: "http://site-a-1:9000"
|
||||
- url: "http://site-a-2:9000"
|
||||
@@ -48,6 +64,14 @@ http:
|
||||
|
||||
otopcua-site-b:
|
||||
loadBalancer:
|
||||
# Blazor Server uses SignalR; the WebSocket upgrade must hit the same
|
||||
# backend that owns the circuit ID. Sticky cookie keeps each session
|
||||
# pinned to one node so the post-handshake WebSocket doesn't 404.
|
||||
sticky:
|
||||
cookie:
|
||||
name: otopcua_lb
|
||||
httpOnly: true
|
||||
sameSite: lax
|
||||
servers:
|
||||
- url: "http://site-b-1:9000"
|
||||
- url: "http://site-b-2:9000"
|
||||
|
||||
@@ -42,6 +42,7 @@ public class AlarmsCommand : CommandBase
|
||||
/// Connects to the server, subscribes to alarm events, and streams operator-facing alarm state changes to the console.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -36,10 +36,7 @@ public class BrowseCommand : CommandBase
|
||||
[CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")]
|
||||
public bool Recursive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints a tree view of the requested address-space branch.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -15,10 +15,7 @@ public class ConnectCommand : CommandBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints the negotiated endpoint details for operator verification.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -56,10 +56,7 @@ public class HistoryReadCommand : CommandBase
|
||||
[CommandOption("interval", Description = "Processing interval in milliseconds for aggregates")]
|
||||
public double IntervalMs { get; init; } = 3600000;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints raw or processed historical values for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -24,10 +24,7 @@ public class ReadCommand : CommandBase
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints the current value, status, and timestamps for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -15,10 +15,8 @@ public class RedundancyCommand : CommandBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints redundancy mode, service level, and partner-server identity data.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <summary>Connects to the server and prints redundancy mode, service level, and partner-server identity data.</summary>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -67,11 +67,7 @@ public class SubscribeCommand : CommandBase
|
||||
[CommandOption("summary-file", Description = "Write summary to this file path on exit (in addition to stdout)")]
|
||||
public string? SummaryFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server, subscribes to <see cref="NodeId" /> (or its subtree when recursive),
|
||||
/// streams data-change notifications to the console, and prints a summary when the command exits.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
@@ -35,6 +35,7 @@ public class WriteCommand : CommandBase
|
||||
/// Connects to the server, converts the supplied value to the node's current data type, and issues the write.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
+3
@@ -12,6 +12,9 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultApplicationConfigurationFactory>();
|
||||
|
||||
/// <summary>Creates an OPC UA application configuration from the provided connection settings.</summary>
|
||||
/// <param name="settings">The connection settings to use.</param>
|
||||
/// <param name="ct">Token to cancel the operation.</param>
|
||||
public async Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct)
|
||||
{
|
||||
// Resolve the canonical PKI path lazily on first use so constructing a
|
||||
|
||||
@@ -11,6 +11,10 @@ internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();
|
||||
|
||||
/// <summary>Selects an OPC UA endpoint matching the requested security mode.</summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL to query.</param>
|
||||
/// <param name="requestedMode">The requested message security mode.</param>
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,14 @@ internal sealed class DefaultSessionFactory : ISessionFactory
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultSessionFactory>();
|
||||
|
||||
/// <summary>Creates a new OPC UA session.</summary>
|
||||
/// <param name="config">The OPC UA application configuration.</param>
|
||||
/// <param name="endpoint">The endpoint description to connect to.</param>
|
||||
/// <param name="sessionName">The name for the session.</param>
|
||||
/// <param name="sessionTimeoutMs">The session timeout in milliseconds.</param>
|
||||
/// <param name="identity">The user identity for the session.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>An adapter wrapping the created session.</returns>
|
||||
public async Task<ISessionAdapter> CreateSessionAsync(
|
||||
ApplicationConfiguration config,
|
||||
EndpointDescription endpoint,
|
||||
|
||||
+2
@@ -11,5 +11,7 @@ internal interface IApplicationConfigurationFactory
|
||||
/// <summary>
|
||||
/// Creates a validated ApplicationConfiguration for the given connection settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The connection settings to configure.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
}
|
||||
@@ -11,6 +11,9 @@ internal interface IEndpointDiscovery
|
||||
/// Discovers endpoints at the given URL and returns the best match for the requested security mode.
|
||||
/// Also rewrites the endpoint URL hostname to match the requested URL when they differ.
|
||||
/// </summary>
|
||||
/// <param name="config">The OPC UA application configuration.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL to discover.</param>
|
||||
/// <param name="requestedMode">The requested message security mode.</param>
|
||||
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ public static class AggregateTypeMapper
|
||||
/// <summary>
|
||||
/// Returns the OPC UA NodeId for the specified aggregate type.
|
||||
/// </summary>
|
||||
/// <param name="aggregate">The aggregate type to map to a NodeId.</param>
|
||||
/// <returns>The OPC UA NodeId for the aggregate function.</returns>
|
||||
public static NodeId ToNodeId(AggregateType aggregate)
|
||||
{
|
||||
return aggregate switch
|
||||
|
||||
@@ -8,9 +8,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
||||
/// </summary>
|
||||
public static class SecurityModeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="SecurityMode" /> to an OPC UA <see cref="MessageSecurityMode" />.
|
||||
/// </summary>
|
||||
/// <summary>Converts a SecurityMode to an OPC UA MessageSecurityMode.</summary>
|
||||
/// <param name="mode">The security mode to convert.</param>
|
||||
/// <returns>The corresponding message security mode.</returns>
|
||||
public static MessageSecurityMode ToMessageSecurityMode(SecurityMode mode)
|
||||
{
|
||||
return mode switch
|
||||
|
||||
@@ -5,5 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
/// </summary>
|
||||
public interface IOpcUaClientServiceFactory
|
||||
{
|
||||
/// <summary>Creates a new OPC UA client service instance.</summary>
|
||||
/// <returns>A new <see cref="IOpcUaClientService"/> instance.</returns>
|
||||
IOpcUaClientService Create();
|
||||
}
|
||||
@@ -5,6 +5,20 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class AlarmEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Initializes a new instance of the <see cref="AlarmEventArgs"/> class.</summary>
|
||||
/// <param name="sourceName">The name of the source object that raised the alarm.</param>
|
||||
/// <param name="conditionName">The condition type name.</param>
|
||||
/// <param name="severity">The alarm severity (0-1000).</param>
|
||||
/// <param name="message">Human-readable alarm message.</param>
|
||||
/// <param name="retain">Whether the alarm should be retained in the display.</param>
|
||||
/// <param name="activeState">Whether the alarm condition is currently active.</param>
|
||||
/// <param name="ackedState">Whether the alarm has been acknowledged.</param>
|
||||
/// <param name="time">The time the event occurred.</param>
|
||||
/// <param name="eventId">The EventId used for alarm acknowledgment.</param>
|
||||
/// <param name="conditionNodeId">The NodeId of the condition instance.</param>
|
||||
/// <param name="operatorComment">Operator-supplied comment on acknowledgment transitions.</param>
|
||||
/// <param name="originalRaiseTimestampUtc">When the alarm originally entered the active state.</param>
|
||||
/// <param name="alarmCategory">Upstream alarm taxonomy bucket (e.g. Process, Safety, Diagnostics).</param>
|
||||
public AlarmEventArgs(
|
||||
string sourceName,
|
||||
string conditionName,
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class BrowseResult
|
||||
{
|
||||
/// <summary>Initializes a new instance of the BrowseResult class.</summary>
|
||||
/// <param name="nodeId">The string representation of the node's NodeId.</param>
|
||||
/// <param name="displayName">The display name of the node.</param>
|
||||
/// <param name="nodeClass">The node class (e.g., "Object", "Variable", "Method").</param>
|
||||
/// <param name="hasChildren">Whether the node has child references.</param>
|
||||
public BrowseResult(string nodeId, string displayName, string nodeClass, bool hasChildren)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
|
||||
@@ -5,6 +5,13 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class ConnectionInfo
|
||||
{
|
||||
/// <summary>Initializes a new instance of the ConnectionInfo with session details.</summary>
|
||||
/// <param name="endpointUrl">The endpoint URL of the connected server.</param>
|
||||
/// <param name="serverName">The server application name.</param>
|
||||
/// <param name="securityMode">The security mode in use.</param>
|
||||
/// <param name="securityPolicyUri">The security policy URI.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="sessionName">The session name.</param>
|
||||
public ConnectionInfo(
|
||||
string endpointUrl,
|
||||
string serverName,
|
||||
|
||||
@@ -5,6 +5,10 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Initializes a new instance of the ConnectionStateChangedEventArgs class.</summary>
|
||||
/// <param name="oldState">The previous connection state.</param>
|
||||
/// <param name="newState">The new connection state.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL associated with the state change.</param>
|
||||
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string endpointUrl)
|
||||
{
|
||||
OldState = oldState;
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class DataChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Initializes a new instance of the DataChangedEventArgs class.</summary>
|
||||
/// <param name="nodeId">The node ID that changed.</param>
|
||||
/// <param name="value">The new data value.</param>
|
||||
public DataChangedEventArgs(string nodeId, DataValue value)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
/// </summary>
|
||||
public sealed class RedundancyInfo
|
||||
{
|
||||
/// <summary>Initializes a new instance of the RedundancyInfo class.</summary>
|
||||
/// <param name="mode">The redundancy mode (e.g., "None", "Cold", "Warm", "Hot").</param>
|
||||
/// <param name="serviceLevel">The server's current service level (0-255).</param>
|
||||
/// <param name="serverUris">URIs of all servers in the redundant set.</param>
|
||||
/// <param name="applicationUri">The application URI of the connected server.</param>
|
||||
public RedundancyInfo(string mode, byte serviceLevel, string[] serverUris, string applicationUri)
|
||||
{
|
||||
Mode = mode;
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
/// </summary>
|
||||
public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
{
|
||||
/// <summary>Creates a new OPC UA client service instance with production adapters.</summary>
|
||||
/// <returns>A new OpcUaClientService instance.</returns>
|
||||
public IOpcUaClientService Create()
|
||||
{
|
||||
return new OpcUaClientService();
|
||||
|
||||
@@ -10,11 +10,13 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI;
|
||||
|
||||
public class App : Application
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
|
||||
@@ -32,35 +32,41 @@ public partial class DateTimeRangePicker : UserControl
|
||||
|
||||
private bool _isUpdating;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="DateTimeRangePicker"/> class.</summary>
|
||||
public DateTimeRangePicker()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the start date and time.</summary>
|
||||
public DateTimeOffset? StartDateTime
|
||||
{
|
||||
get => GetValue(StartDateTimeProperty);
|
||||
set => SetValue(StartDateTimeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the end date and time.</summary>
|
||||
public DateTimeOffset? EndDateTime
|
||||
{
|
||||
get => GetValue(EndDateTimeProperty);
|
||||
set => SetValue(EndDateTimeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the start date/time as formatted text.</summary>
|
||||
public string StartText
|
||||
{
|
||||
get => GetValue(StartTextProperty);
|
||||
set => SetValue(StartTextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the end date/time as formatted text.</summary>
|
||||
public string EndText
|
||||
{
|
||||
get => GetValue(EndTextProperty);
|
||||
set => SetValue(EndTextProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
@@ -82,6 +88,7 @@ public partial class DateTimeRangePicker : UserControl
|
||||
if (lastWeek != null) lastWeek.Click += (_, _) => ApplyPreset(TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Helpers;
|
||||
/// </summary>
|
||||
internal static class StatusCodeFormatter
|
||||
{
|
||||
/// <summary>Formats an OPC UA status code as a hexadecimal code with description.</summary>
|
||||
/// <param name="statusCode">The OPC UA status code to format.</param>
|
||||
/// <returns>A formatted string in the form "0xHEX (description)".</returns>
|
||||
public static string Format(StatusCode statusCode)
|
||||
{
|
||||
var code = statusCode.Code;
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Helpers;
|
||||
/// </summary>
|
||||
internal static class ValueFormatter
|
||||
{
|
||||
/// <summary>Formats an OPC UA value for display, handling arrays and enumerables specially.</summary>
|
||||
/// <param name="value">The value to format, or null.</param>
|
||||
/// <returns>A string representation of the value suitable for display.</returns>
|
||||
public static string Format(object? value)
|
||||
{
|
||||
if (value is null) return "(null)";
|
||||
|
||||
@@ -4,8 +4,11 @@ using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.UI;
|
||||
|
||||
/// <summary>Entry point for the OPC UA client UI application.</summary>
|
||||
public class Program
|
||||
{
|
||||
/// <summary>Main entry point for the application.</summary>
|
||||
/// <param name="args">Command-line arguments passed to the application.</param>
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
@@ -21,6 +24,8 @@ public class Program
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds the Avalonia AppBuilder with platform-specific configuration.</summary>
|
||||
/// <returns>Configured AppBuilder for desktop lifetime.</returns>
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
{
|
||||
return AppBuilder.Configure<App>()
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class AvaloniaUiDispatcher : IUiDispatcher
|
||||
{
|
||||
/// <summary>Posts an action to the Avalonia UI thread for execution.</summary>
|
||||
/// <param name="action">The action to execute on the UI thread.</param>
|
||||
public void Post(Action action)
|
||||
{
|
||||
Dispatcher.UIThread.Post(action);
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public interface ISettingsService
|
||||
{
|
||||
/// <summary>Loads user settings from persistent storage.</summary>
|
||||
UserSettings Load();
|
||||
/// <summary>Saves user settings to persistent storage.</summary>
|
||||
/// <param name="settings">The settings to save.</param>
|
||||
void Save(UserSettings settings);
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ public interface IUiDispatcher
|
||||
/// <summary>
|
||||
/// Posts an action to be executed on the UI thread.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on the UI thread.</param>
|
||||
void Post(Action action);
|
||||
}
|
||||
@@ -19,6 +19,8 @@ public sealed class JsonSettingsService : ISettingsService
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>Loads user settings from the settings file.</summary>
|
||||
/// <returns>The loaded user settings, or a new default instance if load fails.</returns>
|
||||
public UserSettings Load()
|
||||
{
|
||||
try
|
||||
@@ -35,6 +37,8 @@ public sealed class JsonSettingsService : ISettingsService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Saves user settings to the settings file.</summary>
|
||||
/// <param name="settings">The user settings to save.</param>
|
||||
public void Save(UserSettings settings)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class SynchronousUiDispatcher : IUiDispatcher
|
||||
{
|
||||
/// <summary>Executes the action synchronously on the calling thread.</summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
public void Post(Action action)
|
||||
{
|
||||
action();
|
||||
|
||||
@@ -44,6 +44,9 @@ public partial class AlarmsViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private int _activeAlarmCount;
|
||||
|
||||
/// <summary>Initializes a new instance of the AlarmsViewModel class.</summary>
|
||||
/// <param name="service">The OPC UA client service.</param>
|
||||
/// <param name="dispatcher">The UI dispatcher for thread-safe operations.</param>
|
||||
public AlarmsViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
|
||||
{
|
||||
_service = service;
|
||||
@@ -168,6 +171,9 @@ public partial class AlarmsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Acknowledges an alarm and returns (success, message).
|
||||
/// </summary>
|
||||
/// <param name="alarm">The alarm event to acknowledge.</param>
|
||||
/// <param name="comment">Optional comment for the acknowledgment.</param>
|
||||
/// <returns>A tuple with success flag and message.</returns>
|
||||
public async Task<(bool Success, string Message)> AcknowledgeAlarmAsync(AlarmEventViewModel alarm, string comment)
|
||||
{
|
||||
if (!IsConnected || alarm.EventId == null || alarm.ConditionNodeId == null)
|
||||
@@ -197,6 +203,8 @@ public partial class AlarmsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Restores an alarm subscription and requests a condition refresh.
|
||||
/// </summary>
|
||||
/// <param name="sourceNodeId">The source node ID to restore the subscription for.</param>
|
||||
/// <returns>A task that completes when the restore operation finishes.</returns>
|
||||
public async Task RestoreAlarmSubscriptionAsync(string? sourceNodeId)
|
||||
{
|
||||
if (!IsConnected || string.IsNullOrWhiteSpace(sourceNodeId)) return;
|
||||
|
||||
@@ -13,6 +13,11 @@ public class BrowseTreeViewModel : ObservableObject
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly IOpcUaClientService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrowseTreeViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="service">The OPC UA client service.</param>
|
||||
/// <param name="dispatcher">The UI dispatcher for marshaling updates.</param>
|
||||
public BrowseTreeViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
|
||||
{
|
||||
_service = service;
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
|
||||
/// </summary>
|
||||
public class HistoryValueViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>Initializes a new instance of the <see cref="HistoryValueViewModel"/> class.</summary>
|
||||
/// <param name="value">The historical value.</param>
|
||||
/// <param name="status">The status code or text.</param>
|
||||
/// <param name="sourceTimestamp">The source timestamp in string format.</param>
|
||||
/// <param name="serverTimestamp">The server timestamp in string format.</param>
|
||||
public HistoryValueViewModel(string value, string status, string sourceTimestamp, string serverTimestamp)
|
||||
{
|
||||
Value = value;
|
||||
@@ -15,8 +20,12 @@ public class HistoryValueViewModel : ObservableObject
|
||||
ServerTimestamp = serverTimestamp;
|
||||
}
|
||||
|
||||
/// <summary>Gets the historical value.</summary>
|
||||
public string Value { get; }
|
||||
/// <summary>Gets the status code or text.</summary>
|
||||
public string Status { get; }
|
||||
/// <summary>Gets the source timestamp in string format.</summary>
|
||||
public string SourceTimestamp { get; }
|
||||
/// <summary>Gets the server timestamp in string format.</summary>
|
||||
public string ServerTimestamp { get; }
|
||||
}
|
||||
@@ -34,6 +34,9 @@ public partial class HistoryViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private DateTimeOffset? _startTime = DateTimeOffset.UtcNow.AddHours(-1);
|
||||
|
||||
/// <summary>Initializes a new instance of the HistoryViewModel.</summary>
|
||||
/// <param name="service">The OPC UA client service.</param>
|
||||
/// <param name="dispatcher">The UI dispatcher for thread marshalling.</param>
|
||||
public HistoryViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
|
||||
{
|
||||
_service = service;
|
||||
@@ -53,6 +56,7 @@ public partial class HistoryViewModel : ObservableObject
|
||||
AggregateType.StandardDeviation
|
||||
];
|
||||
|
||||
/// <summary>Gets a value indicating whether an aggregate read is selected.</summary>
|
||||
public bool IsAggregateRead => SelectedAggregateType != null;
|
||||
|
||||
/// <summary>History read results.</summary>
|
||||
|
||||
@@ -36,12 +36,16 @@ public partial class ReadWriteViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private string? _writeValue;
|
||||
|
||||
/// <summary>Initializes a new instance of the ReadWriteViewModel class.</summary>
|
||||
/// <param name="service">The OPC UA client service for read/write operations.</param>
|
||||
/// <param name="dispatcher">The UI dispatcher for posting updates to the UI thread.</param>
|
||||
public ReadWriteViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
|
||||
{
|
||||
_service = service;
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
/// <summary>Gets a value indicating whether a node is currently selected.</summary>
|
||||
public bool IsNodeSelected => !string.IsNullOrEmpty(SelectedNodeId);
|
||||
|
||||
partial void OnSelectedNodeIdChanged(string? value)
|
||||
|
||||
@@ -13,6 +13,9 @@ public partial class SubscriptionItemViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private string? _value;
|
||||
|
||||
/// <summary>Initializes a new subscription item with the specified node ID and interval.</summary>
|
||||
/// <param name="nodeId">The OPC UA NodeId to subscribe to.</param>
|
||||
/// <param name="intervalMs">The subscription interval in milliseconds.</param>
|
||||
public SubscriptionItemViewModel(string nodeId, int intervalMs)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
|
||||
@@ -31,6 +31,13 @@ public partial class TreeNodeViewModel : ObservableObject
|
||||
HasChildren = false;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new tree node view model.</summary>
|
||||
/// <param name="nodeId">The OPC UA node identifier.</param>
|
||||
/// <param name="displayName">The display name for this node.</param>
|
||||
/// <param name="nodeClass">The OPC UA node class.</param>
|
||||
/// <param name="hasChildren">Whether this node has child nodes.</param>
|
||||
/// <param name="service">The OPC UA client service for browsing.</param>
|
||||
/// <param name="dispatcher">The UI dispatcher for thread-safe updates.</param>
|
||||
public TreeNodeViewModel(
|
||||
string nodeId,
|
||||
string displayName,
|
||||
|
||||
@@ -10,6 +10,7 @@ public partial class AckAlarmWindow : Window
|
||||
private readonly AlarmsViewModel _alarmsVm;
|
||||
private readonly AlarmEventViewModel _alarm;
|
||||
|
||||
/// <summary>Initializes a new instance of the AckAlarmWindow class for XAML designer support.</summary>
|
||||
public AckAlarmWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -17,6 +18,9 @@ public partial class AckAlarmWindow : Window
|
||||
_alarm = null!;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the AckAlarmWindow class with alarm context.</summary>
|
||||
/// <param name="alarmsVm">The alarms view model.</param>
|
||||
/// <param name="alarm">The alarm event to acknowledge.</param>
|
||||
public AckAlarmWindow(AlarmsViewModel alarmsVm, AlarmEventViewModel alarm)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -16,11 +16,13 @@ public partial class AlarmsView : UserControl
|
||||
private static readonly IBrush HighBrush = new SolidColorBrush(Color.Parse("#FEE2E2")); // light red (666-899)
|
||||
private static readonly IBrush CriticalBrush = new SolidColorBrush(Color.Parse("#FECACA")); // red (900-1000)
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AlarmsView"/> class.</summary>
|
||||
public AlarmsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
||||
|
||||
public partial class BrowseTreeView : UserControl
|
||||
{
|
||||
/// <summary>Initializes a new instance of the BrowseTreeView.</summary>
|
||||
public BrowseTreeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
||||
|
||||
public partial class HistoryView : UserControl
|
||||
{
|
||||
/// <summary>Initializes a new instance of the HistoryView control.</summary>
|
||||
public HistoryView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>Initializes a new instance of the MainWindow, loading the application icon.</summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -51,6 +52,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
@@ -157,6 +159,7 @@ public partial class MainWindow : Window
|
||||
vm.CertificateStorePath = picked;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
||||
|
||||
public partial class ReadWriteView : UserControl
|
||||
{
|
||||
/// <summary>Initializes a new instance of the ReadWriteView class.</summary>
|
||||
public ReadWriteView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -8,11 +8,13 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
||||
|
||||
public partial class SubscriptionsView : UserControl
|
||||
{
|
||||
/// <summary>Initializes a new instance of the <see cref="SubscriptionsView"/> class.</summary>
|
||||
public SubscriptionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
|
||||
@@ -10,6 +10,7 @@ public partial class WriteValueWindow : Window
|
||||
private readonly SubscriptionsViewModel _subscriptionsVm;
|
||||
private readonly string _nodeId;
|
||||
|
||||
/// <summary>Initializes a default instance of the WriteValueWindow for XAML designer support.</summary>
|
||||
public WriteValueWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -17,6 +18,10 @@ public partial class WriteValueWindow : Window
|
||||
_nodeId = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a WriteValueWindow with the node to write and its current value.</summary>
|
||||
/// <param name="subscriptionsVm">The subscriptions view model for write operations.</param>
|
||||
/// <param name="nodeId">The OPC UA node ID to write to.</param>
|
||||
/// <param name="currentValue">The current value of the node, or null if unknown.</param>
|
||||
public WriteValueWindow(SubscriptionsViewModel subscriptionsVm, string nodeId, string? currentValue)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -4,8 +4,13 @@ public sealed class AkkaClusterOptions
|
||||
{
|
||||
public const string SectionName = "Cluster";
|
||||
|
||||
/// <summary>Gets or sets the Akka system name.</summary>
|
||||
public string SystemName { get; set; } = "otopcua";
|
||||
|
||||
/// <summary>Gets or sets the hostname to bind to (default 0.0.0.0).</summary>
|
||||
public string Hostname { get; set; } = "0.0.0.0";
|
||||
|
||||
/// <summary>Gets or sets the port to listen on (default 4053).</summary>
|
||||
public int Port { get; set; } = 4053;
|
||||
|
||||
/// <summary>
|
||||
@@ -15,6 +20,7 @@ public sealed class AkkaClusterOptions
|
||||
/// </summary>
|
||||
public string PublicHostname { get; set; } = "127.0.0.1";
|
||||
|
||||
/// <summary>Gets or sets the seed nodes for cluster bootstrapping.</summary>
|
||||
public string[] SeedNodes { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,6 +25,10 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
private readonly Dictionary<string, HashSet<Member>> _membersByRole = new(StringComparer.Ordinal);
|
||||
private IActorRef? _subscriber;
|
||||
|
||||
/// <summary>Initializes a new instance of the ClusterRoleInfo class.</summary>
|
||||
/// <param name="system">The Akka actor system.</param>
|
||||
/// <param name="options">The cluster configuration options.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public ClusterRoleInfo(ActorSystem system, IOptions<AkkaClusterOptions> options, ILogger<ClusterRoleInfo> logger)
|
||||
{
|
||||
_cluster = Akka.Cluster.Cluster.Get(system);
|
||||
@@ -39,12 +43,20 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
_subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber");
|
||||
}
|
||||
|
||||
/// <summary>Gets the local cluster node identifier.</summary>
|
||||
public CommonsNodeId LocalNode => _localNode;
|
||||
|
||||
/// <summary>Gets the set of roles assigned to the local node.</summary>
|
||||
public IReadOnlySet<string> LocalRoles => _localRoles;
|
||||
|
||||
/// <summary>Checks if the local node has a specific role.</summary>
|
||||
/// <param name="role">The role name to check.</param>
|
||||
/// <returns>True if the local node has the specified role; otherwise false.</returns>
|
||||
public bool HasRole(string role) => _localRoles.Contains(role);
|
||||
|
||||
/// <summary>Gets all cluster members that have a specific role.</summary>
|
||||
/// <param name="role">The role name.</param>
|
||||
/// <returns>A read-only list of node IDs with the specified role.</returns>
|
||||
public IReadOnlyList<CommonsNodeId> MembersWithRole(string role)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -56,6 +68,9 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the current leader node for a specific role.</summary>
|
||||
/// <param name="role">The role name.</param>
|
||||
/// <returns>The node ID of the current role leader, or null if no leader is elected.</returns>
|
||||
public CommonsNodeId? RoleLeader(string role)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -66,6 +81,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Occurs when the leader for a role changes.</summary>
|
||||
public event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged;
|
||||
|
||||
private void SeedFromCurrentState()
|
||||
@@ -91,6 +107,8 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles a cluster member event (member up/removed).</summary>
|
||||
/// <param name="evt">The member event from the cluster.</param>
|
||||
internal void HandleMemberEvent(ClusterEvent.IMemberEvent evt)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -114,6 +132,8 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles a role leader change event.</summary>
|
||||
/// <param name="evt">The role leader changed event from the cluster.</param>
|
||||
internal void HandleRoleLeaderChanged(ClusterEvent.RoleLeaderChanged evt)
|
||||
{
|
||||
CommonsNodeId? previous = null;
|
||||
@@ -156,6 +176,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
private static CommonsNodeId ToNodeId(Akka.Actor.Address address) =>
|
||||
CommonsNodeId.Parse($"{address.Host ?? string.Empty}:{address.Port ?? 0}");
|
||||
|
||||
/// <summary>Disposes the ClusterRoleInfo and stops the subscriber actor.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_subscriber?.Tell(PoisonPill.Instance);
|
||||
@@ -164,6 +185,8 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
|
||||
private sealed class SubscriberActor : ReceiveActor
|
||||
{
|
||||
/// <summary>Initializes a new instance of the SubscriberActor class.</summary>
|
||||
/// <param name="owner">The ClusterRoleInfo instance to forward events to.</param>
|
||||
public SubscriberActor(ClusterRoleInfo owner)
|
||||
{
|
||||
Receive<ClusterEvent.IMemberEvent>(e => owner.HandleMemberEvent(e));
|
||||
@@ -172,6 +195,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
Receive<ClusterEvent.CurrentClusterState>(_ => { /* seeded from initial snapshot */ });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PreStart()
|
||||
{
|
||||
Akka.Cluster.Cluster.Get(Context.System).Subscribe(
|
||||
@@ -182,6 +206,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
typeof(ClusterEvent.RoleLeaderChanged));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PostStop() =>
|
||||
Akka.Cluster.Cluster.Get(Context.System).Unsubscribe(Self);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
|
||||
/// <summary>Loads embedded HOCON configuration resources.</summary>
|
||||
public static class HoconLoader
|
||||
{
|
||||
private const string ResourceName = "ZB.MOM.WW.OtOpcUa.Cluster.Resources.akka.conf";
|
||||
|
||||
/// <summary>Loads the base Akka configuration from embedded resources.</summary>
|
||||
/// <returns>The loaded HOCON configuration as a string.</returns>
|
||||
public static string LoadBaseConfig()
|
||||
{
|
||||
using var stream = typeof(HoconLoader).Assembly.GetManifestResourceStream(ResourceName)
|
||||
|
||||
@@ -7,6 +7,8 @@ public static class RoleParser
|
||||
"admin", "driver", "dev",
|
||||
};
|
||||
|
||||
/// <summary>Parses a comma-separated string of role names into a validated array.</summary>
|
||||
/// <param name="raw">The raw role string to parse.</param>
|
||||
public static string[] Parse(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
|
||||
|
||||
@@ -16,6 +16,8 @@ public static class ServiceCollectionExtensions
|
||||
/// configurator via <see cref="WithOtOpcUaClusterBootstrap"/> — keeping the entire Akka graph
|
||||
/// under Akka.Hosting's management so cluster singletons land on the same ActorSystem.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to configure.</param>
|
||||
/// <param name="configuration">The application configuration containing cluster options.</param>
|
||||
public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<AkkaClusterOptions>()
|
||||
@@ -41,6 +43,8 @@ public static class ServiceCollectionExtensions
|
||||
/// });
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="builder">The Akka configuration builder to configure.</param>
|
||||
/// <param name="serviceProvider">The service provider for resolving cluster options.</param>
|
||||
public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap(
|
||||
this AkkaConfigurationBuilder builder,
|
||||
IServiceProvider serviceProvider)
|
||||
|
||||
@@ -10,7 +10,14 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
/// </summary>
|
||||
public interface IAlarmActorStateStore
|
||||
{
|
||||
/// <summary>Loads the persisted state snapshot for an alarm actor.</summary>
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The alarm state snapshot if found; null if the alarm has no persisted state.</returns>
|
||||
Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct);
|
||||
/// <summary>Saves the alarm actor state snapshot.</summary>
|
||||
/// <param name="snapshot">The state snapshot to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -34,8 +41,14 @@ public sealed class NullAlarmActorStateStore : IAlarmActorStateStore
|
||||
{
|
||||
public static readonly NullAlarmActorStateStore Instance = new();
|
||||
private NullAlarmActorStateStore() { }
|
||||
/// <summary>Always returns null, indicating no persisted state.</summary>
|
||||
/// <param name="alarmId">The alarm identifier (unused).</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct) =>
|
||||
Task.FromResult<AlarmActorStateSnapshot?>(null);
|
||||
/// <summary>Completes immediately without persisting anything.</summary>
|
||||
/// <param name="snapshot">The state snapshot (ignored).</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
/// </summary>
|
||||
public interface IScriptedAlarmEvaluator
|
||||
{
|
||||
/// <summary>Evaluates an alarm predicate against the provided dependencies.</summary>
|
||||
/// <param name="alarmId">The unique identifier of the alarm being evaluated.</param>
|
||||
/// <param name="predicate">The predicate expression to evaluate.</param>
|
||||
/// <param name="dependencies">Read-only dictionary of variable names to values for predicate evaluation.</param>
|
||||
/// <returns>Result containing success flag, alarm active state, and optional failure reason.</returns>
|
||||
ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary<string, object?> dependencies);
|
||||
}
|
||||
|
||||
@@ -15,7 +20,14 @@ public interface IScriptedAlarmEvaluator
|
||||
/// <c>Success</c> is true; on failure the caller should keep the prior state and log Reason.</summary>
|
||||
public sealed record ScriptedAlarmEvalResult(bool Success, bool Active, string? Reason)
|
||||
{
|
||||
/// <summary>Creates a successful alarm evaluation result with the given active state.</summary>
|
||||
/// <param name="active">Whether the alarm condition is active.</param>
|
||||
/// <returns>A successful evaluation result.</returns>
|
||||
public static ScriptedAlarmEvalResult Ok(bool active) => new(true, active, null);
|
||||
|
||||
/// <summary>Creates a failed alarm evaluation result with the given reason.</summary>
|
||||
/// <param name="reason">Description of the evaluation failure cause.</param>
|
||||
/// <returns>A failed evaluation result.</returns>
|
||||
public static ScriptedAlarmEvalResult Failure(string reason) => new(false, false, reason);
|
||||
}
|
||||
|
||||
@@ -25,6 +37,11 @@ public sealed class NullScriptedAlarmEvaluator : IScriptedAlarmEvaluator
|
||||
{
|
||||
public static readonly NullScriptedAlarmEvaluator Instance = new();
|
||||
private NullScriptedAlarmEvaluator() { }
|
||||
/// <summary>Returns an inactive alarm result for every evaluation (safe no-op behavior).</summary>
|
||||
/// <param name="alarmId">The alarm identifier (ignored).</param>
|
||||
/// <param name="predicate">The predicate expression (ignored).</param>
|
||||
/// <param name="dependencies">The variable dependencies (ignored).</param>
|
||||
/// <returns>Always returns an inactive alarm result.</returns>
|
||||
public ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary<string, object?> dependencies)
|
||||
=> ScriptedAlarmEvalResult.Ok(active: false);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ public interface IVirtualTagEvaluator
|
||||
/// <paramref name="dependencies"/>. Implementations must not throw — script failures
|
||||
/// are reported via <see cref="VirtualTagEvalResult.Failure"/>.
|
||||
/// </summary>
|
||||
/// <param name="virtualTagId">The unique identifier of the virtual tag being evaluated.</param>
|
||||
/// <param name="expression">The expression string to evaluate.</param>
|
||||
/// <param name="dependencies">Read-only dictionary of variable names to values for expression evaluation.</param>
|
||||
/// <returns>Result containing success flag, evaluated value, and optional failure reason.</returns>
|
||||
VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies);
|
||||
}
|
||||
|
||||
@@ -21,7 +25,15 @@ public interface IVirtualTagEvaluator
|
||||
public sealed record VirtualTagEvalResult(bool Success, object? Value, string? Reason)
|
||||
{
|
||||
public static readonly VirtualTagEvalResult NoChange = new(true, null, "no-change");
|
||||
|
||||
/// <summary>Creates a successful evaluation result with the given value.</summary>
|
||||
/// <param name="value">The evaluated value.</param>
|
||||
/// <returns>A successful evaluation result.</returns>
|
||||
public static VirtualTagEvalResult Ok(object? value) => new(true, value, null);
|
||||
|
||||
/// <summary>Creates a failed evaluation result with the given reason.</summary>
|
||||
/// <param name="reason">Description of the failure cause.</param>
|
||||
/// <returns>A failed evaluation result.</returns>
|
||||
public static VirtualTagEvalResult Failure(string reason) => new(false, null, reason);
|
||||
}
|
||||
|
||||
@@ -31,6 +43,11 @@ public sealed class NullVirtualTagEvaluator : IVirtualTagEvaluator
|
||||
{
|
||||
public static readonly NullVirtualTagEvaluator Instance = new();
|
||||
private NullVirtualTagEvaluator() { }
|
||||
/// <summary>Returns <see cref="VirtualTagEvalResult.NoChange"/> for every evaluation.</summary>
|
||||
/// <param name="virtualTagId">The virtual tag identifier (ignored).</param>
|
||||
/// <param name="expression">The expression string (ignored).</param>
|
||||
/// <param name="dependencies">The variable dependencies (ignored).</param>
|
||||
/// <returns>Always returns <see cref="VirtualTagEvalResult.NoChange"/>.</returns>
|
||||
public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
|
||||
=> VirtualTagEvalResult.NoChange;
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
/// </summary>
|
||||
public interface IAdminOperationsClient
|
||||
{
|
||||
/// <summary>Starts a new deployment on the cluster-singleton admin operations actor.</summary>
|
||||
/// <param name="createdBy">The user or system identifier triggering the deployment.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation containing the deployment start result.</returns>
|
||||
Task<StartDeploymentResult> StartDeploymentAsync(string createdBy, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,23 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
/// </summary>
|
||||
public interface IClusterRoleInfo
|
||||
{
|
||||
/// <summary>Gets the local cluster node identifier.</summary>
|
||||
NodeId LocalNode { get; }
|
||||
/// <summary>Gets the set of roles assigned to the local node.</summary>
|
||||
IReadOnlySet<string> LocalRoles { get; }
|
||||
/// <summary>Checks if the local node has the specified role.</summary>
|
||||
/// <param name="role">Role name to check.</param>
|
||||
/// <returns>True if the local node has the role; otherwise, false.</returns>
|
||||
bool HasRole(string role);
|
||||
/// <summary>Gets all nodes assigned to the specified role.</summary>
|
||||
/// <param name="role">Role name to query.</param>
|
||||
/// <returns>List of node identifiers with the role.</returns>
|
||||
IReadOnlyList<NodeId> MembersWithRole(string role);
|
||||
/// <summary>Gets the leader node for the specified role, or null if no leader is elected.</summary>
|
||||
/// <param name="role">Role name to query.</param>
|
||||
/// <returns>The leader node identifier, or null if no leader exists.</returns>
|
||||
NodeId? RoleLeader(string role);
|
||||
|
||||
/// <summary>Occurs when the leader of a role changes.</summary>
|
||||
event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged;
|
||||
}
|
||||
|
||||
@@ -8,5 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
/// </summary>
|
||||
public interface IFleetDiagnosticsClient
|
||||
{
|
||||
/// <summary>Gets diagnostics for the specified node.</summary>
|
||||
/// <param name="nodeId">The node ID to retrieve diagnostics for.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
Task<NodeDiagnosticsSnapshot> GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
|
||||
/// <summary>Event arguments for role leader change notifications.</summary>
|
||||
public sealed class RoleLeaderChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Gets the role name that changed leadership.</summary>
|
||||
public required string Role { get; init; }
|
||||
|
||||
/// <summary>Gets the previous leader node ID, or null if there was no previous leader.</summary>
|
||||
public required NodeId? PreviousLeader { get; init; }
|
||||
|
||||
/// <summary>Gets the new leader node ID, or null if the role is now leaderless.</summary>
|
||||
public required NodeId? NewLeader { get; init; }
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ public static class OtOpcUaTelemetry
|
||||
/// Starts a deploy span tagged with the deployment id. Caller disposes to close. Returns
|
||||
/// null when no listener is attached so the call site stays cheap on undecorated builds.
|
||||
/// </summary>
|
||||
/// <param name="deploymentId">The deployment identifier to tag the span with.</param>
|
||||
public static Activity? StartDeployApplySpan(string deploymentId)
|
||||
{
|
||||
var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal);
|
||||
|
||||
@@ -18,17 +18,41 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
|
||||
/// <summary>Swap in the production sink. Pass <c>null</c> to revert to the null sink
|
||||
/// (used during graceful shutdown so post-stop writes don't hit a half-disposed manager).</summary>
|
||||
/// <param name="sink">The sink implementation to use, or null to use the null sink.</param>
|
||||
public void SetSink(IOpcUaAddressSpaceSink? sink) =>
|
||||
_inner = sink ?? NullOpcUaAddressSpaceSink.Instance;
|
||||
|
||||
/// <summary>Writes a value to the OPC UA address space through the inner sink.</summary>
|
||||
/// <param name="nodeId">The node ID of the variable.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality value.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Writes an alarm state through the inner sink.</summary>
|
||||
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
||||
/// <param name="active">Whether the alarm is active.</param>
|
||||
/// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteAlarmState(alarmNodeId, active, acknowledged, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Ensures a folder exists in the address space through the inner sink.</summary>
|
||||
/// <param name="folderNodeId">The node ID of the folder.</param>
|
||||
/// <param name="parentNodeId">The node ID of the parent folder, or null for root.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName);
|
||||
|
||||
/// <summary>Ensures a variable exists in the address space through the inner sink.</summary>
|
||||
/// <param name="variableNodeId">The node ID of the variable.</param>
|
||||
/// <param name="parentFolderNodeId">The node ID of the parent folder, or null for root.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="dataType">The OPC UA data type of the variable.</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
|
||||
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType);
|
||||
|
||||
/// <summary>Rebuilds the address space through the inner sink.</summary>
|
||||
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ public sealed class DeferredServiceLevelPublisher : IServiceLevelPublisher
|
||||
private volatile IServiceLevelPublisher _inner = NullServiceLevelPublisher.Instance;
|
||||
|
||||
/// <summary>Swap the underlying publisher. Pass null to revert to the Null no-op.</summary>
|
||||
/// <param name="inner">The publisher implementation to use, or null to use the null publisher.</param>
|
||||
public void SetInner(IServiceLevelPublisher? inner) =>
|
||||
_inner = inner ?? NullServiceLevelPublisher.Instance;
|
||||
|
||||
/// <summary>Publishes a service level value to the inner publisher.</summary>
|
||||
/// <param name="serviceLevel">The service level to publish.</param>
|
||||
public void Publish(byte serviceLevel) => _inner.Publish(serviceLevel);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,17 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
public interface IOpcUaAddressSpaceSink
|
||||
{
|
||||
/// <summary>Write a Variable node's current value + quality + source timestamp.</summary>
|
||||
/// <param name="nodeId">The OPC UA node ID of the variable.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The quality status of the value.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc);
|
||||
|
||||
/// <summary>Write an alarm-condition Variable's active/acknowledged state.</summary>
|
||||
/// <param name="alarmNodeId">The OPC UA node ID of the alarm.</param>
|
||||
/// <param name="active">Whether the alarm is active.</param>
|
||||
/// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc);
|
||||
|
||||
/// <summary>
|
||||
@@ -20,8 +28,24 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <paramref name="parentNodeId"/> is null the folder is parented under the namespace
|
||||
/// root. Idempotent: calling twice with the same id is safe.
|
||||
/// </summary>
|
||||
/// <param name="folderNodeId">The OPC UA node ID for the folder.</param>
|
||||
/// <param name="parentNodeId">The parent folder node ID, or null for namespace root.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName);
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
|
||||
/// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
|
||||
/// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
|
||||
/// Used by <c>Phase7Applier</c> to materialise Galaxy / SystemPlatform tags ahead of any
|
||||
/// driver-side subscribe so OPC UA clients can browse them. Idempotent.
|
||||
/// </summary>
|
||||
/// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
|
||||
/// <param name="parentFolderNodeId">The parent folder node ID, or null for namespace root.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="dataType">OPC UA built-in type name ("Boolean" / "Int32" / "Float" / etc.).</param>
|
||||
void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType);
|
||||
|
||||
/// <summary>
|
||||
/// Tear down + repopulate the address space. Called by <c>OpcUaPublishActor</c> after a
|
||||
/// successful deployment apply so the node manager reflects the new config. Idempotent.
|
||||
@@ -39,8 +63,19 @@ public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public static readonly NullOpcUaAddressSpaceSink Instance = new();
|
||||
private NullOpcUaAddressSpaceSink() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() { }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
/// </summary>
|
||||
public interface IServiceLevelPublisher
|
||||
{
|
||||
/// <summary>Publishes the service level value to the OPC UA Server object.</summary>
|
||||
/// <param name="serviceLevel">The service level value (0-255).</param>
|
||||
void Publish(byte serviceLevel);
|
||||
}
|
||||
|
||||
@@ -17,6 +19,11 @@ public sealed class NullServiceLevelPublisher : IServiceLevelPublisher
|
||||
{
|
||||
public static readonly NullServiceLevelPublisher Instance = new();
|
||||
private NullServiceLevelPublisher() { }
|
||||
|
||||
/// <summary>Gets the last published service level value.</summary>
|
||||
public byte LastPublished { get; private set; }
|
||||
|
||||
/// <summary>Records the service level value without publishing.</summary>
|
||||
/// <param name="serviceLevel">The service level value (0-255).</param>
|
||||
public void Publish(byte serviceLevel) => LastPublished = serviceLevel;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
public readonly record struct CorrelationId(Guid Value)
|
||||
{
|
||||
/// <summary>Creates a new CorrelationId with a randomly generated GUID.</summary>
|
||||
public static CorrelationId NewId() => new(Guid.NewGuid());
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value.ToString("N");
|
||||
/// <summary>Parses a lowercase hex string without hyphens into a CorrelationId.</summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
public static CorrelationId Parse(string s) => new(Guid.ParseExact(s, "N"));
|
||||
/// <summary>Attempts to parse a lowercase hex string without hyphens into a CorrelationId.</summary>
|
||||
/// <param name="s">The string to parse, or null.</param>
|
||||
/// <param name="id">The resulting CorrelationId if parsing succeeds.</param>
|
||||
public static bool TryParse(string? s, out CorrelationId id)
|
||||
{
|
||||
if (Guid.TryParseExact(s, "N", out var g)) { id = new CorrelationId(g); return true; }
|
||||
|
||||
@@ -2,9 +2,22 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
public readonly record struct DeploymentId(Guid Value)
|
||||
{
|
||||
/// <summary>Creates a new deployment ID with a random GUID.</summary>
|
||||
/// <returns>A new DeploymentId.</returns>
|
||||
public static DeploymentId NewId() => new(Guid.NewGuid());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value.ToString("N");
|
||||
|
||||
/// <summary>Parses a deployment ID from a hex string without hyphens.</summary>
|
||||
/// <param name="s">The hex string to parse.</param>
|
||||
/// <returns>The parsed DeploymentId.</returns>
|
||||
public static DeploymentId Parse(string s) => new(Guid.ParseExact(s, "N"));
|
||||
|
||||
/// <summary>Attempts to parse a deployment ID from a hex string without hyphens.</summary>
|
||||
/// <param name="s">The hex string to parse, or null.</param>
|
||||
/// <param name="id">The parsed DeploymentId if successful, or default.</param>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public static bool TryParse(string? s, out DeploymentId id)
|
||||
{
|
||||
if (Guid.TryParseExact(s, "N", out var g)) { id = new DeploymentId(g); return true; }
|
||||
|
||||
@@ -2,9 +2,22 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
public readonly record struct ExecutionId(Guid Value)
|
||||
{
|
||||
/// <summary>Creates a new execution ID with a randomly generated GUID.</summary>
|
||||
/// <returns>A new ExecutionId instance.</returns>
|
||||
public static ExecutionId NewId() => new(Guid.NewGuid());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value.ToString("N");
|
||||
|
||||
/// <summary>Parses the specified string into an ExecutionId in format N.</summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <returns>The parsed ExecutionId.</returns>
|
||||
public static ExecutionId Parse(string s) => new(Guid.ParseExact(s, "N"));
|
||||
|
||||
/// <summary>Tries to parse the specified string into an ExecutionId in format N.</summary>
|
||||
/// <param name="s">The string to parse, or null.</param>
|
||||
/// <param name="id">The parsed ExecutionId, or default if parsing fails.</param>
|
||||
/// <returns>true if parsing succeeded; otherwise, false.</returns>
|
||||
public static bool TryParse(string? s, out ExecutionId id)
|
||||
{
|
||||
if (Guid.TryParseExact(s, "N", out var g)) { id = new ExecutionId(g); return true; }
|
||||
|
||||
@@ -7,11 +7,22 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
/// </summary>
|
||||
public readonly record struct NodeId(string Value)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value;
|
||||
|
||||
/// <summary>Parses a string into a NodeId.</summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <returns>A new NodeId instance.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the string is null, empty, or whitespace.</exception>
|
||||
public static NodeId Parse(string s) =>
|
||||
string.IsNullOrWhiteSpace(s)
|
||||
? throw new ArgumentException("NodeId value cannot be empty.", nameof(s))
|
||||
: new NodeId(s);
|
||||
|
||||
/// <summary>Attempts to parse a string into a NodeId.</summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <param name="id">The parsed NodeId if successful.</param>
|
||||
/// <returns>True if the parse succeeded; otherwise false.</returns>
|
||||
public static bool TryParse(string? s, out NodeId id)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(s)) { id = new NodeId(s); return true; }
|
||||
|
||||
@@ -6,11 +6,24 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
/// </summary>
|
||||
public readonly record struct RevisionHash(string Value)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value;
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="RevisionHash"/>.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <returns>A <see cref="RevisionHash"/> instance.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the input is null, empty, or whitespace.</exception>
|
||||
public static RevisionHash Parse(string s) =>
|
||||
string.IsNullOrWhiteSpace(s)
|
||||
? throw new ArgumentException("RevisionHash value cannot be empty.", nameof(s))
|
||||
: new RevisionHash(s);
|
||||
/// <summary>
|
||||
/// Attempts to parse a string into a <see cref="RevisionHash"/>.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <param name="hash">The parsed hash, or default if parsing fails.</param>
|
||||
/// <returns>True if parsing succeeded; otherwise false.</returns>
|
||||
public static bool TryParse(string? s, out RevisionHash hash)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(s)) { hash = new RevisionHash(s); return true; }
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
/// </remarks>
|
||||
public sealed class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<OtOpcUaConfigDbContext>
|
||||
{
|
||||
/// <summary>Creates a new DbContext instance for design-time operations.</summary>
|
||||
/// <param name="args">Command-line arguments (unused).</param>
|
||||
/// <returns>The configured DbContext instance.</returns>
|
||||
public OtOpcUaConfigDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var connection = Environment.GetEnvironmentVariable("OTOPCUA_CONFIG_CONNECTION");
|
||||
|
||||
@@ -6,13 +6,16 @@ public sealed class ClusterNode
|
||||
/// <summary>Stable per-machine logical ID, e.g. "LINE3-OPCUA-A".</summary>
|
||||
public required string NodeId { get; set; }
|
||||
|
||||
/// <summary>The unique identifier of the cluster this node belongs to.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Machine hostname / IP.</summary>
|
||||
public required string Host { get; set; }
|
||||
|
||||
/// <summary>The OPC UA server port (default 4840).</summary>
|
||||
public int OpcUaPort { get; set; } = 4840;
|
||||
|
||||
/// <summary>The dashboard HTTP port (default 8081).</summary>
|
||||
public int DashboardPort { get; set; } = 8081;
|
||||
|
||||
/// <summary>
|
||||
@@ -32,15 +35,21 @@ public sealed class ClusterNode
|
||||
/// </summary>
|
||||
public string? DriverConfigOverridesJson { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this node is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets the timestamp when this node was last seen.</summary>
|
||||
public DateTime? LastSeenAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the timestamp when this node was created.</summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the username of who created this node.</summary>
|
||||
public required string CreatedBy { get; set; }
|
||||
|
||||
// Navigation
|
||||
/// <summary>Gets or sets the cluster this node belongs to.</summary>
|
||||
public ServerCluster? Cluster { get; set; }
|
||||
/// <summary>Gets or sets the credentials associated with this node.</summary>
|
||||
public ICollection<ClusterNodeCredential> Credentials { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -8,22 +8,30 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class ClusterNodeCredential
|
||||
{
|
||||
/// <summary>Gets or sets the credential identifier.</summary>
|
||||
public Guid CredentialId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the node identifier this credential binds to.</summary>
|
||||
public required string NodeId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the credential kind (login, certificate, etc.).</summary>
|
||||
public required CredentialKind Kind { get; set; }
|
||||
|
||||
/// <summary>Login name / cert thumbprint / SID / gMSA name.</summary>
|
||||
/// <summary>Gets or sets the credential value (login name / cert thumbprint / SID / gMSA name).</summary>
|
||||
public required string Value { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the credential is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets the date/time when the credential was last rotated.</summary>
|
||||
public DateTime? RotatedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the date/time when the credential was created.</summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the user who created the credential.</summary>
|
||||
public required string CreatedBy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the related cluster node.</summary>
|
||||
public ClusterNode? Node { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,21 +6,28 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class ConfigAuditLog
|
||||
{
|
||||
/// <summary>Gets or sets the unique audit log identifier.</summary>
|
||||
public long AuditId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the timestamp of the audit event.</summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the principal (user or service) that initiated the event.</summary>
|
||||
public required string Principal { get; set; }
|
||||
|
||||
/// <summary>DraftCreated | DraftEdited | Published | RolledBack | NodeApplied | CredentialAdded | CredentialDisabled | ClusterCreated | NodeAdded | ExternalIdReleased | CrossClusterNamespaceAttempt | OpcUaAccessDenied | …</summary>
|
||||
public required string EventType { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster identifier associated with the event, if applicable.</summary>
|
||||
public string? ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the node identifier associated with the event, if applicable.</summary>
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the generation identifier associated with the event, if applicable.</summary>
|
||||
public long? GenerationId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets additional event details in JSON format.</summary>
|
||||
public string? DetailsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,21 +7,27 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class ConfigEdit
|
||||
{
|
||||
/// <summary>Gets the unique identifier for this edit.</summary>
|
||||
public Guid EditId { get; init; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>Gets the type of entity that was edited.</summary>
|
||||
public required string EntityType { get; init; }
|
||||
|
||||
/// <summary>Gets the identifier of the entity that was edited.</summary>
|
||||
public Guid EntityId { get; init; }
|
||||
|
||||
/// <summary>JSON payload of the column-name → new-value pairs touched by this edit.</summary>
|
||||
/// <summary>Gets the JSON payload of the column-name → new-value pairs touched by this edit.</summary>
|
||||
public required string FieldsJson { get; init; }
|
||||
|
||||
/// <summary>Optional correlation across edits inside a single admin operation.</summary>
|
||||
/// <summary>Gets the optional correlation identifier across edits inside a single admin operation.</summary>
|
||||
public Guid? ExecutionId { get; init; }
|
||||
|
||||
/// <summary>Gets the username of the user who performed the edit.</summary>
|
||||
public required string EditedBy { get; init; }
|
||||
|
||||
/// <summary>Gets the UTC timestamp when the edit was performed.</summary>
|
||||
public DateTime EditedAtUtc { get; init; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets the node identifier of the admin instance that performed the edit.</summary>
|
||||
public required string SourceNode { get; init; }
|
||||
}
|
||||
|
||||
@@ -10,21 +10,30 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class Deployment
|
||||
{
|
||||
/// <summary>Gets or sets the unique deployment identifier.</summary>
|
||||
public Guid DeploymentId { get; init; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>Gets or sets the revision hash of the deployment artifact.</summary>
|
||||
public required string RevisionHash { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the deployment status.</summary>
|
||||
public DeploymentStatus Status { get; set; } = DeploymentStatus.Dispatching;
|
||||
|
||||
/// <summary>Gets or sets the name of the user who created the deployment.</summary>
|
||||
public required string CreatedBy { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the deployment was created.</summary>
|
||||
public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the serialized artifact blob containing the configuration.</summary>
|
||||
public byte[] ArtifactBlob { get; init; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the row version for optimistic concurrency control.</summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the failure reason if the deployment failed.</summary>
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the deployment was sealed.</summary>
|
||||
public DateTime? SealedAtUtc { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,15 +3,27 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// <summary>Per-device row for multi-device drivers (Modbus, AB CIP). Optional for single-device drivers.</summary>
|
||||
public sealed class Device
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique database row identifier for the device.
|
||||
/// </summary>
|
||||
public Guid DeviceRowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device identifier.
|
||||
/// </summary>
|
||||
public required string DeviceId { get; set; }
|
||||
|
||||
/// <summary>Logical FK to <see cref="DriverInstance.DriverInstanceId"/>.</summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device name.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the device is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Schemaless per-driver-type device config (host, port, unit ID, slot, etc.).</summary>
|
||||
|
||||
@@ -39,6 +39,7 @@ public sealed class DriverHostStatus
|
||||
/// </summary>
|
||||
public required string HostName { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the current connectivity state of the host.</summary>
|
||||
public DriverHostState State { get; set; } = DriverHostState.Unknown;
|
||||
|
||||
/// <summary>Timestamp of the last state transition (not of the most recent heartbeat).</summary>
|
||||
|
||||
@@ -3,10 +3,13 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// <summary>One driver instance in a cluster's generation. JSON config is schemaless per-driver-type.</summary>
|
||||
public sealed class DriverInstance
|
||||
{
|
||||
/// <summary>Gets or sets the row ID for this driver instance.</summary>
|
||||
public Guid DriverInstanceRowId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the unique driver instance identifier.</summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster ID this driver instance belongs to.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -15,11 +18,13 @@ public sealed class DriverInstance
|
||||
/// </summary>
|
||||
public required string NamespaceId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the friendly name of this driver instance.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Galaxy | ModbusTcp | AbCip | AbLegacy | S7 | TwinCat | Focas | OpcUaClient</summary>
|
||||
public required string DriverType { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this driver instance is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time (decision #91).</summary>
|
||||
@@ -46,5 +51,6 @@ public sealed class DriverInstance
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the related server cluster for navigation.</summary>
|
||||
public ServerCluster? Cluster { get; set; }
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </remarks>
|
||||
public sealed class DriverInstanceResilienceStatus
|
||||
{
|
||||
/// <summary>Gets or sets the driver instance identifier.</summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
/// <summary>Gets or sets the host name.</summary>
|
||||
public required string HostName { get; set; }
|
||||
|
||||
/// <summary>Most recent time the circuit breaker for this (instance, host) opened; null if never.</summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class Equipment
|
||||
{
|
||||
/// <summary>Gets or sets the row identifier for this equipment.</summary>
|
||||
public Guid EquipmentRowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -43,19 +44,29 @@ public sealed class Equipment
|
||||
|
||||
// OPC UA Companion Spec OPC 40010 Machinery Identification fields (decision #139).
|
||||
// All nullable so equipment can be added before identity is fully captured.
|
||||
/// <summary>Gets or sets the manufacturer name for this equipment.</summary>
|
||||
public string? Manufacturer { get; set; }
|
||||
/// <summary>Gets or sets the model number or designation for this equipment.</summary>
|
||||
public string? Model { get; set; }
|
||||
/// <summary>Gets or sets the serial number for this equipment.</summary>
|
||||
public string? SerialNumber { get; set; }
|
||||
/// <summary>Gets or sets the hardware revision level for this equipment.</summary>
|
||||
public string? HardwareRevision { get; set; }
|
||||
/// <summary>Gets or sets the software revision level for this equipment.</summary>
|
||||
public string? SoftwareRevision { get; set; }
|
||||
/// <summary>Gets or sets the year of construction for this equipment.</summary>
|
||||
public short? YearOfConstruction { get; set; }
|
||||
/// <summary>Gets or sets the asset location information for this equipment.</summary>
|
||||
public string? AssetLocation { get; set; }
|
||||
/// <summary>Gets or sets the manufacturer URI for this equipment.</summary>
|
||||
public string? ManufacturerUri { get; set; }
|
||||
/// <summary>Gets or sets the device manual URI for this equipment.</summary>
|
||||
public string? DeviceManualUri { get; set; }
|
||||
|
||||
/// <summary>Nullable hook for future schemas-repo template ID (decision #112).</summary>
|
||||
public string? EquipmentClassRef { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether this equipment is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
@@ -17,15 +17,31 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </remarks>
|
||||
public sealed class EquipmentImportBatch
|
||||
{
|
||||
/// <summary>Gets or sets the unique identifier for this batch.</summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster identifier.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the user name who created this batch.</summary>
|
||||
public required string CreatedBy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when this batch was created.</summary>
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the total number of rows staged in this batch.</summary>
|
||||
public int RowsStaged { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of rows accepted in this batch.</summary>
|
||||
public int RowsAccepted { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of rows rejected in this batch.</summary>
|
||||
public int RowsRejected { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when this batch was finalised, or null if still in staging.</summary>
|
||||
public DateTime? FinalisedAtUtc { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the collection of staged rows in this batch.</summary>
|
||||
public ICollection<EquipmentImportRow> Rows { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -37,32 +53,74 @@ public sealed class EquipmentImportBatch
|
||||
/// </summary>
|
||||
public sealed class EquipmentImportRow
|
||||
{
|
||||
/// <summary>Gets or sets the unique identifier for this row.</summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the parent batch identifier.</summary>
|
||||
public Guid BatchId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the line number in the source file.</summary>
|
||||
public int LineNumberInFile { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this row was accepted.</summary>
|
||||
public bool IsAccepted { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the reason this row was rejected, if applicable.</summary>
|
||||
public string? RejectReason { get; set; }
|
||||
|
||||
// Required (decision #117)
|
||||
/// <summary>Gets or sets the Z tag identifier.</summary>
|
||||
public required string ZTag { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the machine code.</summary>
|
||||
public required string MachineCode { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the SAP identifier.</summary>
|
||||
public required string SAPID { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the equipment identifier.</summary>
|
||||
public required string EquipmentId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the equipment UUID.</summary>
|
||||
public required string EquipmentUuid { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the equipment name.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UNS area name.</summary>
|
||||
public required string UnsAreaName { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UNS line name.</summary>
|
||||
public required string UnsLineName { get; set; }
|
||||
|
||||
// Optional (decision #139 — OPC 40010 Identification)
|
||||
/// <summary>Gets or sets the manufacturer name.</summary>
|
||||
public string? Manufacturer { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the equipment model.</summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the serial number.</summary>
|
||||
public string? SerialNumber { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the hardware revision.</summary>
|
||||
public string? HardwareRevision { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the software revision.</summary>
|
||||
public string? SoftwareRevision { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the year of construction.</summary>
|
||||
public string? YearOfConstruction { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the asset location.</summary>
|
||||
public string? AssetLocation { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the manufacturer URI.</summary>
|
||||
public string? ManufacturerUri { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the device manual URI.</summary>
|
||||
public string? DeviceManualUri { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the parent batch.</summary>
|
||||
public EquipmentImportBatch? Batch { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class ExternalIdReservation
|
||||
{
|
||||
/// <summary>Gets or sets the unique reservation identifier.</summary>
|
||||
public Guid ReservationId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the kind of reservation (ZTag or SAPID).</summary>
|
||||
public required ReservationKind Kind { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the reserved external ID value.</summary>
|
||||
public required string Value { get; set; }
|
||||
|
||||
/// <summary>The equipment that owns this reservation. Stays bound even when equipment is disabled.</summary>
|
||||
@@ -21,16 +24,21 @@ public sealed class ExternalIdReservation
|
||||
/// <summary>First cluster to publish this reservation.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the timestamp when the reservation was first published.</summary>
|
||||
public DateTime FirstPublishedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the identifier of the user or system that first published the reservation.</summary>
|
||||
public required string FirstPublishedBy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the timestamp of the most recent publication.</summary>
|
||||
public DateTime LastPublishedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Non-null when explicitly released by FleetAdmin (audit-logged, requires reason).</summary>
|
||||
public DateTime? ReleasedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the identifier of the user or system that released the reservation.</summary>
|
||||
public string? ReleasedBy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the reason for releasing the reservation.</summary>
|
||||
public string? ReleaseReason { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,24 +8,30 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class Namespace
|
||||
{
|
||||
/// <summary>Gets or sets the row identifier for this namespace.</summary>
|
||||
public Guid NamespaceRowId { get; set; }
|
||||
|
||||
/// <summary>Stable logical ID, e.g. "LINE3-OPCUA-equipment". Globally unique in v2.</summary>
|
||||
public required string NamespaceId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster identifier.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the namespace kind.</summary>
|
||||
public required NamespaceKind Kind { get; set; }
|
||||
|
||||
/// <summary>E.g. "urn:zb:warsaw-west:equipment". Unique fleet-wide per generation.</summary>
|
||||
public required string NamespaceUri { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the namespace is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets optional notes about the namespace.</summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the associated server cluster.</summary>
|
||||
public ServerCluster? Cluster { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,14 +8,19 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class NodeAcl
|
||||
{
|
||||
/// <summary>Gets or sets the database row ID for this ACL entry.</summary>
|
||||
public Guid NodeAclRowId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the logical ID of this ACL entry.</summary>
|
||||
public required string NodeAclId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster ID for this ACL entry.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the LDAP group for this ACL entry.</summary>
|
||||
public required string LdapGroup { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the scope kind for this ACL entry.</summary>
|
||||
public required NodeAclScopeKind ScopeKind { get; set; }
|
||||
|
||||
/// <summary>NULL when <see cref="ScopeKind"/> = <see cref="NodeAclScopeKind.Cluster"/>; otherwise the scoped entity's logical ID.</summary>
|
||||
@@ -24,6 +29,7 @@ public sealed class NodeAcl
|
||||
/// <summary>Bitmask of <see cref="NodePermissions"/>. Stored as int in SQL.</summary>
|
||||
public required NodePermissions PermissionFlags { get; set; }
|
||||
|
||||
/// <summary>Gets or sets optional notes for this ACL entry.</summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
@@ -10,20 +10,29 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class NodeDeploymentState
|
||||
{
|
||||
/// <summary>Gets or sets the cluster node identifier.</summary>
|
||||
public required string NodeId { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the deployment identifier.</summary>
|
||||
public Guid DeploymentId { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the deployment status on this node.</summary>
|
||||
public NodeDeploymentStatus Status { get; set; } = NodeDeploymentStatus.Applying;
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the deployment application started.</summary>
|
||||
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the deployment was successfully applied, or null if not yet applied.</summary>
|
||||
public DateTime? AppliedAtUtc { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the failure reason if the deployment failed, or null if successful.</summary>
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the row version for optimistic concurrency control.</summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the cluster node entity reference.</summary>
|
||||
public ClusterNode? Node { get; set; }
|
||||
/// <summary>Gets or sets the deployment entity reference.</summary>
|
||||
public Deployment? Deployment { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// <summary>Driver-scoped polling group. Tags reference it via <see cref="Tag.PollGroupId"/>.</summary>
|
||||
public sealed class PollGroup
|
||||
{
|
||||
/// <summary>Gets or sets the database row identifier for the polling group.</summary>
|
||||
public Guid PollGroupRowId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the unique identifier for the polling group.</summary>
|
||||
public required string PollGroupId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the driver instance that owns this polling group.</summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the display name of the polling group.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the poll interval in milliseconds.</summary>
|
||||
public int IntervalMs { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </remarks>
|
||||
public sealed class Script
|
||||
{
|
||||
/// <summary>Gets or sets the script row identifier.</summary>
|
||||
public Guid ScriptRowId { get; set; }
|
||||
|
||||
/// <summary>Stable logical id. Globally unique in v2.</summary>
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </remarks>
|
||||
public sealed class ScriptedAlarm
|
||||
{
|
||||
/// <summary>Gets or sets the database row identifier for this scripted alarm.</summary>
|
||||
public Guid ScriptedAlarmRowId { get; set; }
|
||||
|
||||
/// <summary>Stable logical id — drives <c>AlarmConditionType.ConditionName</c>. Globally unique in v2.</summary>
|
||||
@@ -52,6 +53,7 @@ public sealed class ScriptedAlarm
|
||||
/// </summary>
|
||||
public bool Retain { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this alarm is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
@@ -45,13 +45,16 @@ public sealed class ScriptedAlarmState
|
||||
/// <summary>Operator-supplied ack comment. Null if no comment or never acked.</summary>
|
||||
public string? LastAckComment { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp of the last acknowledgment.</summary>
|
||||
public DateTime? LastAckUtc { get; set; }
|
||||
|
||||
/// <summary>User who last confirmed.</summary>
|
||||
public string? LastConfirmUser { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the operator-supplied confirm comment. Null if no comment or never confirmed.</summary>
|
||||
public string? LastConfirmComment { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp of the last confirmation.</summary>
|
||||
public DateTime? LastConfirmUtc { get; set; }
|
||||
|
||||
/// <summary>JSON array of operator comments, append-only (GxP audit).</summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class ServerCluster
|
||||
/// <summary>Stable logical ID, e.g. "LINE3-OPCUA".</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the display name for the server cluster.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>UNS level 1. Canonical org value: "zb" per decision #140.</summary>
|
||||
@@ -19,23 +20,33 @@ public sealed class ServerCluster
|
||||
/// <summary>UNS level 2, e.g. "warsaw-west".</summary>
|
||||
public required string Site { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of nodes in the cluster.</summary>
|
||||
public byte NodeCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the redundancy mode for the cluster.</summary>
|
||||
public required RedundancyMode RedundancyMode { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the cluster is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets optional notes about the cluster.</summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the cluster was created.</summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Gets or sets the user who created the cluster.</summary>
|
||||
public required string CreatedBy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UTC timestamp when the cluster was last modified.</summary>
|
||||
public DateTime? ModifiedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the user who last modified the cluster.</summary>
|
||||
public string? ModifiedBy { get; set; }
|
||||
|
||||
// Navigation
|
||||
/// <summary>Gets or sets the collection of cluster nodes.</summary>
|
||||
public ICollection<ClusterNode> Nodes { get; set; } = [];
|
||||
/// <summary>Gets or sets the collection of namespaces in the cluster.</summary>
|
||||
public ICollection<Namespace> Namespaces { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -9,12 +9,24 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
public sealed class Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique database row identifier for the tag.
|
||||
/// </summary>
|
||||
public Guid TagRowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tag identifier.
|
||||
/// </summary>
|
||||
public required string TagId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the driver instance identifier for this tag.
|
||||
/// </summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device identifier.
|
||||
/// </summary>
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -23,6 +35,9 @@ public sealed class Tag
|
||||
/// </summary>
|
||||
public string? EquipmentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tag name.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Only used when <see cref="EquipmentId"/> is NULL (SystemPlatform namespace).</summary>
|
||||
@@ -31,11 +46,17 @@ public sealed class Tag
|
||||
/// <summary>OPC UA built-in type name (Boolean / Int32 / Float / etc.).</summary>
|
||||
public required string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the access level for this tag.
|
||||
/// </summary>
|
||||
public required TagAccessLevel AccessLevel { get; set; }
|
||||
|
||||
/// <summary>Per decisions #44–45 — opt-in for write retry eligibility.</summary>
|
||||
public bool WriteIdempotent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the poll group identifier for batching read/write operations.
|
||||
/// </summary>
|
||||
public string? PollGroupId { get; set; }
|
||||
|
||||
/// <summary>Register address / scaling / poll group / byte-order / etc. — schemaless per driver type.</summary>
|
||||
|
||||
@@ -3,19 +3,24 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// <summary>UNS level-3 segment. Generation-versioned per decision #115.</summary>
|
||||
public sealed class UnsArea
|
||||
{
|
||||
/// <summary>Gets or sets the unique row identifier.</summary>
|
||||
public Guid UnsAreaRowId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the UNS area identifier.</summary>
|
||||
public required string UnsAreaId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the cluster identifier.</summary>
|
||||
public required string ClusterId { get; set; }
|
||||
|
||||
/// <summary>UNS level 3 segment: matches <c>^[a-z0-9-]{1,32}$</c> OR equals literal <c>_default</c>.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets optional notes for the area.</summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets or sets the associated server cluster.</summary>
|
||||
public ServerCluster? Cluster { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// <summary>UNS level-4 segment. Generation-versioned per decision #115.</summary>
|
||||
public sealed class UnsLine
|
||||
{
|
||||
/// <summary>Gets or sets the unique row identifier for this UNS line.</summary>
|
||||
public Guid UnsLineRowId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the unique identifier for this UNS line.</summary>
|
||||
public required string UnsLineId { get; set; }
|
||||
|
||||
/// <summary>Logical FK to <see cref="UnsArea.UnsAreaId"/>.</summary>
|
||||
@@ -13,6 +15,7 @@ public sealed class UnsLine
|
||||
/// <summary>UNS level 4 segment: matches <c>^[a-z0-9-]{1,32}$</c> OR equals literal <c>_default</c>.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets optional notes describing this UNS line.</summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
@@ -20,9 +20,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </remarks>
|
||||
public sealed class VirtualTag
|
||||
{
|
||||
/// <summary>Gets or sets the database row ID (primary key).</summary>
|
||||
public Guid VirtualTagRowId { get; set; }
|
||||
|
||||
/// <summary>Stable logical id. Globally unique in v2.</summary>
|
||||
/// <summary>Gets or sets the stable logical identifier, globally unique in v2.</summary>
|
||||
public required string VirtualTagId { get; set; }
|
||||
|
||||
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this virtual tag.</summary>
|
||||
@@ -43,9 +44,10 @@ public sealed class VirtualTag
|
||||
/// <summary>Timer re-evaluation cadence in milliseconds. <c>null</c> = no timer.</summary>
|
||||
public int? TimerIntervalMs { get; set; }
|
||||
|
||||
/// <summary>Per plan decision #10 — checkbox to route this tag's values through <c>IHistoryWriter</c>.</summary>
|
||||
/// <summary>Gets or sets a value indicating whether this tag's values should be historized.</summary>
|
||||
public bool Historize { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this virtual tag is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user