From f05b5d79c44dee3fb4030726db62df2e34745542 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 22:53:59 -0400 Subject: [PATCH] fix(galaxy): AdviseSupervisory before raw Write so writes commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain MXAccess Write runs with no user login (WriteUserId is typically 0), and MXAccess only COMMITS such a write when the item is advised in supervisory mode. Without it the gateway's Write call doesn't throw (the reply looks OK) but the value never reaches the galaxy. GatewayGalaxyDataWriter now issues AdviseSupervisory (once per item handle) before each raw Write; SecuredWrite/ VerifiedWrite tags keep their own user-identity path. Live-verified end-to-end: an authorized write to a Galaxy equipment tag commits and PERSISTS across a fresh re-subscribe; an anonymous write is denied. (The sister ScadaBridge driver commits writes the other way — a configured non-zero WriteUserId + regular Advise; we have no galaxy login, so we use the supervisory context.) --- .../Runtime/GatewayGalaxyDataWriter.cs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs index 905253e8..2574f1e5 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs @@ -27,6 +27,9 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter private readonly ILogger _logger; private readonly ConcurrentDictionary _itemHandles = new(StringComparer.OrdinalIgnoreCase); + // Item handles we've already AdviseSupervisory'd this session — supervisory advise is + // idempotent but the round-trip isn't free, so do it once per handle (see EnsureSupervisoryAdvisedAsync). + private readonly ConcurrentDictionary _supervisedHandles = new(); /// Initializes a new Galaxy data writer. /// The MXAccess gateway session. @@ -77,9 +80,24 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter .ConfigureAwait(false); var mxValue = MxValueEncoder.Encode(request.Value); - var reply = NeedsSecuredWrite(classification) - ? await InvokeWriteSecuredAsync(session, serverHandle, itemHandle, mxValue, ct).ConfigureAwait(false) - : await session.WriteRawAsync(serverHandle, itemHandle, mxValue, _writeUserId, ct).ConfigureAwait(false); + MxCommandReply reply; + if (NeedsSecuredWrite(classification)) + { + // SecuredWrite/VerifiedWrite tags carry their own ArchestrA user identity + // (current/verifier user), so they don't use the supervisory path. + reply = await InvokeWriteSecuredAsync(session, serverHandle, itemHandle, mxValue, ct) + .ConfigureAwait(false); + } + else + { + // A raw Write runs with NO user login (WriteUserId is typically 0), so MXAccess + // only COMMITS the value when the item is advised in SUPERVISORY mode. Without it + // the gateway's Write call doesn't throw (reply looks OK) but the value never + // reaches the galaxy. AdviseSupervisory once per handle, then Write. + await EnsureSupervisoryAdvisedAsync(session, serverHandle, itemHandle, ct).ConfigureAwait(false); + reply = await session.WriteRawAsync(serverHandle, itemHandle, mxValue, _writeUserId, ct) + .ConfigureAwait(false); + } return TranslateReply(reply, request.FullReference); } @@ -110,6 +128,48 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter return handle; } + /// + /// Advise an item in MXAccess AdviseSupervisory mode. A plain Write runs + /// under no user login ( is typically 0); MXAccess only + /// COMMITS such a write when the item is supervisory-advised. (A logged-in identity — + /// i.e. a non-zero WriteUserId, as the ScadaBridge sister driver uses — would commit via + /// a regular Advise instead; we don't log in, so we use the supervisory context.) + /// The gateway client doesn't expose a typed method, so we build the + /// and route through InvokeAsync (same pattern as ). + /// Idempotent per handle for the session lifetime. + /// + private async Task EnsureSupervisoryAdvisedAsync( + MxGatewaySession session, int serverHandle, int itemHandle, CancellationToken ct) + { + if (!_supervisedHandles.TryAdd(itemHandle, 0)) return; + + var request = new MxCommandRequest + { + SessionId = session.SessionId, + ClientCorrelationId = Guid.NewGuid().ToString("N"), + Command = new MxCommand + { + Kind = MxCommandKind.AdviseSupervisory, + AdviseSupervisory = new AdviseSupervisoryCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + }, + }; + + var reply = await session.InvokeAsync(request, ct).ConfigureAwait(false); + if (reply.ProtocolStatus is { } proto && proto.Code != ProtocolStatusCode.Ok) + { + // Supervisory advise failed — forget it so the next write retries, and let the + // write proceed (it surfaces its own status via TranslateReply). + _supervisedHandles.TryRemove(itemHandle, out _); + _logger.LogWarning( + "GalaxyDriver supervisory advise failed for item {ItemHandle}: {Code} {Message}", + itemHandle, proto.Code, proto.Message); + } + } + /// /// Issue a WriteSecured command. The high-level session client doesn't expose /// WriteSecuredAsync as a typed method — we build the