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