From 9eedf9d6a968c023e4df293bed3a5ad7daee9bd3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 20:14:48 -0400 Subject: [PATCH] clients: document supervisory/array-write parity gotchas and add advise-supervisory to all CLIs A consuming project hit two MXAccess parity surprises: a plain Write only records its user_id when the item has an active supervisory advise (the path to take when not authenticating), and array writes replace the whole array rather than patching individual elements. Document both across the five client READMEs and gateway.md's compatibility baseline, and expose the missing advise-supervisory subcommand in the go/python/rust/java CLIs (plus the .NET help text) so callers can establish the supervisory advise without dropping to the raw command API. --- clients/dotnet/README.md | 47 +++++++++++++++++++ .../MxGatewayClientCli.cs | 25 ++++++++++ clients/go/README.md | 46 ++++++++++++++++++ clients/go/cmd/mxgw-go/main.go | 40 ++++++++++++++++ clients/java/README.md | 40 ++++++++++++++++ .../zb/mom/ww/mxgateway/cli/MxGatewayCli.java | 43 +++++++++++++++++ .../ww/mxgateway/cli/MxGatewayCliTests.java | 9 ++++ clients/python/README.md | 43 +++++++++++++++++ .../src/zb_mom_ww_mxgateway_cli/commands.py | 33 +++++++++++++ clients/rust/README.md | 43 +++++++++++++++++ clients/rust/crates/mxgw-cli/src/main.rs | 42 +++++++++++++++-- gateway.md | 9 ++++ 12 files changed, 416 insertions(+), 4 deletions(-) diff --git a/clients/dotnet/README.md b/clients/dotnet/README.md index 87455aa..1625e0c 100644 --- a/clients/dotnet/README.md +++ b/clients/dotnet/README.md @@ -121,6 +121,53 @@ can keep the full `MxCommandReply`, HRESULT, and status array when MXAccess itself rejects a command. `MxAccessException.Reply` contains the raw generated reply. +## Write Semantics And Common Pitfalls + +These are MXAccess parity behaviors that surprise new callers. The gateway +forwards them unchanged — it does not paper over them. + +### Attributing a write to a user without `AuthenticateUser` + +MXAccess only stamps a plain `Write`/`Write2` with a Galaxy user id when the +item carries an active *supervisory* advise. If you are **not** using the +verified/secured path (`AuthenticateUser` → `WriteSecured`/`WriteSecured2`) but +still need the write attributed to a user id, you must first advise the item +supervisory and then pass that user id on the write. Without the supervisory +advise the `userId` on a plain write is ignored. + +The library exposes `Advise`/`UnAdvise` as named helpers but not supervisory +advise, so send it through the generic command channel: + +```csharp +await session.InvokeAsync(new MxCommandRequest +{ + SessionId = session.SessionId, + Command = new MxCommand + { + Kind = MxCommandKind.AdviseSupervisory, + AdviseSupervisory = new AdviseSupervisoryCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + }, +}); + +await session.WriteAsync(serverHandle, itemHandle, value.ToMxValue(), userId); +``` + +The CLI exposes the same command as `advise-supervisory`, and `write` / +`write2` take `--user-id`. + +### Array writes replace the whole array + +A write to an array attribute **replaces the entire array**; it is not an +element-wise patch. To change a subset of elements, send the full array with +the unchanged elements included. For example, to change 2 elements of a +20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus +the 2 new ones). Sending only the 2 changed values overwrites the attribute +with a 2-element array. + ## CLI Usage The test CLI supports deterministic JSON output for automation: diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs index e163420..93e69e1 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs @@ -110,6 +110,8 @@ public static class MxGatewayClientCli .ConfigureAwait(false), "advise" => await AdviseAsync(arguments, client, standardOutput, cancellation.Token) .ConfigureAwait(false), + "advise-supervisory" => await AdviseSupervisoryAsync(arguments, client, standardOutput, cancellation.Token) + .ConfigureAwait(false), "subscribe-bulk" => await SubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token) .ConfigureAwait(false), "unsubscribe-bulk" => await UnsubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token) @@ -451,6 +453,28 @@ public static class MxGatewayClientCli cancellationToken); } + private static Task AdviseSupervisoryAsync( + CliArguments arguments, + IMxGatewayCliClient client, + TextWriter output, + CancellationToken cancellationToken) + { + return InvokeAndWriteAsync( + arguments, + client, + output, + new MxCommand + { + Kind = MxCommandKind.AdviseSupervisory, + AdviseSupervisory = new AdviseSupervisoryCommand + { + ServerHandle = arguments.GetInt32("server-handle"), + ItemHandle = arguments.GetInt32("item-handle"), + }, + }, + cancellationToken); + } + private static Task SubscribeBulkAsync( CliArguments arguments, IMxGatewayCliClient client, @@ -2066,6 +2090,7 @@ public static class MxGatewayClientCli writer.WriteLine("mxgw-dotnet register --session-id --client-name [--json]"); writer.WriteLine("mxgw-dotnet add-item --session-id --server-handle --item [--json]"); writer.WriteLine("mxgw-dotnet advise --session-id --server-handle --item-handle [--json]"); + writer.WriteLine("mxgw-dotnet advise-supervisory --session-id --server-handle --item-handle [--json]"); writer.WriteLine("mxgw-dotnet subscribe-bulk --session-id --server-handle --items [--json]"); writer.WriteLine("mxgw-dotnet unsubscribe-bulk --session-id --server-handle --item-handles [--json]"); writer.WriteLine("mxgw-dotnet read-bulk --session-id --server-handle --items [--timeout-ms ] [--json]"); diff --git a/clients/go/README.md b/clients/go/README.md index 9aa0da8..fa0b33d 100644 --- a/clients/go/README.md +++ b/clients/go/README.md @@ -99,6 +99,52 @@ call returns a `StreamAlarmsClient`; cancel its context to terminate the stream. All three pass straight through to the gateway's central alarm monitor. +## Write Semantics And Common Pitfalls + +These are MXAccess parity behaviors that surprise new callers. The gateway +forwards them unchanged — it does not paper over them. + +### Attributing a write to a user without `AuthenticateUser` + +MXAccess only stamps a plain `Write`/`Write2` with a Galaxy user id when the +item carries an active *supervisory* advise. If you are **not** using the +verified/secured path (`AuthenticateUser` → `WriteSecured`/`WriteSecured2`) but +still need the write attributed to a user id, you must first advise the item +supervisory and then pass that user id on the write. Without the supervisory +advise the `userID` on a plain write is ignored. + +The session exposes `Advise`/`UnAdvise` but not supervisory advise, so send it +through the generic command channel: + +```go +_, err := client.Invoke(ctx, &pb.MxCommandRequest{ + SessionId: session.ID(), + Command: &pb.MxCommand{ + Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY, + Payload: &pb.MxCommand_AdviseSupervisory{ + AdviseSupervisory: &pb.AdviseSupervisoryCommand{ + ServerHandle: serverHandle, + ItemHandle: itemHandle, + }, + }, + }, +}) + +err = session.Write(ctx, serverHandle, itemHandle, value, userID) +``` + +The CLI exposes the same command as `advise-supervisory`, and `write` / +`write2` take `--user-id`. + +### Array writes replace the whole array + +A write to an array attribute **replaces the entire array**; it is not an +element-wise patch. To change a subset of elements, send the full array with +the unchanged elements included. For example, to change 2 elements of a +20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus +the 2 new ones). Sending only the 2 changed values overwrites the attribute +with a 2-element array. + ## Galaxy Repository browse The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a diff --git a/clients/go/cmd/mxgw-go/main.go b/clients/go/cmd/mxgw-go/main.go index 11fadb1..d2dfda1 100644 --- a/clients/go/cmd/mxgw-go/main.go +++ b/clients/go/cmd/mxgw-go/main.go @@ -21,6 +21,7 @@ import ( "syscall" "time" + pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" @@ -87,6 +88,8 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err return runAddItem(ctx, args[1:], stdout, stderr) case "advise": return runAdvise(ctx, args[1:], stdout, stderr) + case "advise-supervisory": + return runAdviseSupervisory(ctx, args[1:], stdout, stderr) case "subscribe-bulk": return runSubscribeBulk(ctx, args[1:], stdout, stderr) case "unsubscribe-bulk": @@ -358,6 +361,43 @@ func runAdvise(ctx context.Context, args []string, stdout, stderr io.Writer) err return writeCommandOutput(stdout, *jsonOutput, "advise", options, reply, err) } +func runAdviseSupervisory(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("advise-supervisory", flag.ContinueOnError) + flags.SetOutput(stderr) + common := bindCommonFlags(flags) + jsonOutput := flags.Bool("json", false, "write JSON output") + sessionID := flags.String("session-id", "", "gateway session id") + serverHandle := flags.Int("server-handle", 0, "MXAccess server handle") + itemHandle := flags.Int("item-handle", 0, "MXAccess item handle") + + if err := flags.Parse(args); err != nil { + return err + } + if *sessionID == "" { + return errors.New("session-id is required") + } + + client, options, err := dialForCommand(ctx, common) + if err != nil { + return err + } + defer client.Close() + + reply, err := client.Invoke(ctx, &pb.MxCommandRequest{ + SessionId: *sessionID, + Command: &pb.MxCommand{ + Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY, + Payload: &pb.MxCommand_AdviseSupervisory{ + AdviseSupervisory: &pb.AdviseSupervisoryCommand{ + ServerHandle: int32(*serverHandle), + ItemHandle: int32(*itemHandle), + }, + }, + }, + }) + return writeCommandOutput(stdout, *jsonOutput, "advise-supervisory", options, reply, err) +} + func runSubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error { flags := flag.NewFlagSet("subscribe-bulk", flag.ContinueOnError) flags.SetOutput(stderr) diff --git a/clients/java/README.md b/clients/java/README.md index d94d11a..04e50de 100644 --- a/clients/java/README.md +++ b/clients/java/README.md @@ -84,6 +84,46 @@ yields alarm-feed messages from the gateway's central monitor), and `acknowledgeAlarm` (ack by full alarm reference with an optional comment and ack target). Close the subscription to cancel the underlying gRPC stream. +## Write Semantics And Common Pitfalls + +These are MXAccess parity behaviors that surprise new callers. The gateway +forwards them unchanged — it does not paper over them. + +### Attributing a write to a user without `authenticateUser` + +MXAccess only stamps a plain `write`/`write2` with a Galaxy user id when the +item carries an active *supervisory* advise. If you are **not** using the +verified/secured path (`authenticateUser` → `writeSecured`/`writeSecured2`) but +still need the write attributed to a user id, you must first advise the item +supervisory and then pass that user id on the write. Without the supervisory +advise the `userId` on a plain write is ignored. + +The session exposes `advise`/`unAdvise` but not supervisory advise, so send it +through the generic command channel: + +```java +session.invokeCommand(MxCommand.newBuilder() + .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY) + .setAdviseSupervisory(AdviseSupervisoryCommand.newBuilder() + .setServerHandle(serverHandle) + .setItemHandle(itemHandle)) + .build()); + +session.write(serverHandle, itemHandle, value, userId); +``` + +The CLI exposes the same command as `advise-supervisory`, and `write` / +`write2` take `--user-id`. + +### Array writes replace the whole array + +A write to an array attribute **replaces the entire array**; it is not an +element-wise patch. To change a subset of elements, send the full array with +the unchanged elements included. For example, to change 2 elements of a +20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus +the 2 new ones). Sending only the 2 changed values overwrites the attribute +with a 2-element array. + ## Galaxy Repository Browse The Galaxy Repository service is a separate metadata-only gRPC service exposed diff --git a/clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java b/clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java index 44b4483..32ef7a5 100644 --- a/clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java +++ b/clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java @@ -153,6 +153,8 @@ public final class MxGatewayCli implements Callable { commandLine.addSubcommand("register", new RegisterCommand(clientFactory)); commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory)); commandLine.addSubcommand("advise", new AdviseCommand(clientFactory)); + commandLine.addSubcommand( + "advise-supervisory", new AdviseSupervisoryCommand(clientFactory)); commandLine.addSubcommand("subscribe-bulk", new SubscribeBulkCommand(clientFactory)); commandLine.addSubcommand("unsubscribe-bulk", new UnsubscribeBulkCommand(clientFactory)); commandLine.addSubcommand("read-bulk", new ReadBulkCommand(clientFactory)); @@ -1044,6 +1046,34 @@ public final class MxGatewayCli implements Callable { } } + @Command( + name = "advise-supervisory", + description = "Invokes MXAccess AdviseSupervisory.") + static final class AdviseSupervisoryCommand extends GatewayCommand { + @Option(names = "--session-id", required = true, description = "Gateway session id.") + String sessionId; + + @Option(names = "--server-handle", required = true, description = "MXAccess server handle.") + int serverHandle; + + @Option(names = "--item-handle", required = true, description = "MXAccess item handle.") + int itemHandle; + + AdviseSupervisoryCommand(MxGatewayCliClientFactory clientFactory) { + super(clientFactory); + } + + @Override + public Integer call() { + try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) { + MxCommandReply reply = + client.session(sessionId).adviseSupervisoryRaw(serverHandle, itemHandle); + writeOutput("advise-supervisory", common, json, reply, () -> reply.getKind().name()); + } + return 0; + } + } + @Command(name = "subscribe-bulk", description = "Invokes MXAccess SubscribeBulk.") static final class SubscribeBulkCommand extends GatewayCommand { @Option(names = "--session-id", required = true, description = "Gateway session id.") @@ -1830,6 +1860,8 @@ public final class MxGatewayCli implements Callable { MxCommandReply adviseRaw(int serverHandle, int itemHandle); + MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle); + MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId); List subscribeBulk(int serverHandle, List items); @@ -1948,6 +1980,17 @@ public final class MxGatewayCli implements Callable { return session.adviseRaw(serverHandle, itemHandle); } + @Override + public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) { + return session.invokeCommand(MxCommand.newBuilder() + .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY) + .setAdviseSupervisory( + mxaccess_gateway.v1.MxaccessGateway.AdviseSupervisoryCommand.newBuilder() + .setServerHandle(serverHandle) + .setItemHandle(itemHandle)) + .build()); + } + @Override public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) { return session.writeRaw(serverHandle, itemHandle, value, userId); diff --git a/clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java b/clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java index fc2c2c5..6c31a5e 100644 --- a/clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java +++ b/clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java @@ -1302,6 +1302,15 @@ final class MxGatewayCliTests { .build(); } + @Override + public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) { + adviseCalled = true; + return MxCommandReply.newBuilder() + .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY) + .setProtocolStatus(ok()) + .build(); + } + @Override public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) { lastWriteValue = value; diff --git a/clients/python/README.md b/clients/python/README.md index f340e3e..4ad1e84 100644 --- a/clients/python/README.md +++ b/clients/python/README.md @@ -105,6 +105,49 @@ terminate the stream. Canceling a Python task cancels the client-side gRPC call or stream wait. It does not abort an in-flight MXAccess COM call inside the worker process. +## Write Semantics And Common Pitfalls + +These are MXAccess parity behaviors that surprise new callers. The gateway +forwards them unchanged — it does not paper over them. + +### Attributing a write to a user without `authenticate_user` + +MXAccess only stamps a plain `write`/`write2` with a Galaxy user id when the +item carries an active *supervisory* advise. If you are **not** using the +verified/secured path (`authenticate_user` → `write_secured`/`write_secured2`) +but still need the write attributed to a user id, you must first advise the +item supervisory and then pass that user id on the write. Without the +supervisory advise the `user_id` on a plain write is ignored. + +The session exposes `advise`/`unadvise` but not supervisory advise, so send it +through the generic command channel: + +```python +await session.invoke( + pb.MxCommand( + kind=pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY, + advise_supervisory=pb.AdviseSupervisoryCommand( + server_handle=server_handle, + item_handle=item_handle, + ), + ) +) + +await session.write(server_handle, item_handle, value, user_id=user_id) +``` + +The CLI exposes the same command as `advise-supervisory`, and `write` / +`write2` take `--user-id`. + +### Array writes replace the whole array + +A write to an array attribute **replaces the entire array**; it is not an +element-wise patch. To change a subset of elements, send the full array with +the unchanged elements included. For example, to change 2 elements of a +20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus +the 2 new ones). Sending only the 2 changed values overwrites the attribute +with a 2-element array. + ## Galaxy Repository Browse The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC diff --git a/clients/python/src/zb_mom_ww_mxgateway_cli/commands.py b/clients/python/src/zb_mom_ww_mxgateway_cli/commands.py index 97da512..99876ad 100644 --- a/clients/python/src/zb_mom_ww_mxgateway_cli/commands.py +++ b/clients/python/src/zb_mom_ww_mxgateway_cli/commands.py @@ -277,6 +277,23 @@ def advise(**kwargs: Any) -> None: _run(_advise(**kwargs), output_json=kwargs["output_json"], secrets=_secrets(kwargs)) +@main.command("advise-supervisory") +@gateway_options +@click.option("--session-id", required=True, help="Gateway session id.") +@click.option("--server-handle", required=True, type=int, help="MXAccess server handle.") +@click.option("--item-handle", required=True, type=int, help="MXAccess item handle.") +@click.option("--correlation-id", default="", help="Client correlation id.") +@click.option("--json", "output_json", is_flag=True, help="Emit JSON output.") +def advise_supervisory(**kwargs: Any) -> None: + """Invoke MXAccess AdviseSupervisory.""" + + _run( + _advise_supervisory(**kwargs), + output_json=kwargs["output_json"], + secrets=_secrets(kwargs), + ) + + @main.command("subscribe-bulk") @gateway_options @click.option("--session-id", required=True, help="Gateway session id.") @@ -725,6 +742,22 @@ async def _advise(**kwargs: Any) -> dict[str, Any]: return {"ok": True} +async def _advise_supervisory(**kwargs: Any) -> dict[str, Any]: + async with await _connect(kwargs) as client: + session = _session(client, kwargs["session_id"]) + await session.invoke( + pb.MxCommand( + kind=pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY, + advise_supervisory=pb.AdviseSupervisoryCommand( + server_handle=kwargs["server_handle"], + item_handle=kwargs["item_handle"], + ), + ), + correlation_id=kwargs["correlation_id"], + ) + return {"ok": True} + + async def _subscribe_bulk(**kwargs: Any) -> dict[str, Any]: async with await _connect(kwargs) as client: session = _session(client, kwargs["session_id"]) diff --git a/clients/rust/README.md b/clients/rust/README.md index 27779e9..934898e 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -125,6 +125,49 @@ preserving the raw message for parity diagnostics. Command replies whose protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and retain the raw `MxCommandReply`. +## Write Semantics And Common Pitfalls + +These are MXAccess parity behaviors that surprise new callers. The gateway +forwards them unchanged — it does not paper over them. + +### Attributing a write to a user without `authenticate_user` + +MXAccess only stamps a plain `write`/`write2` with a Galaxy user id when the +item carries an active *supervisory* advise. If you are **not** using the +verified/secured path (`authenticate_user` → `write_secured`/`write_secured2`) +but still need the write attributed to a user id, you must first advise the +item supervisory and then pass that user id on the write. Without the +supervisory advise the `user_id` on a plain write is ignored. + +The session exposes `advise`/`un_advise` but not supervisory advise, so send it +through the generic command channel: + +```rust +session + .invoke( + MxCommandKind::AdviseSupervisory, + Payload::AdviseSupervisory(AdviseSupervisoryCommand { + server_handle, + item_handle, + }), + ) + .await?; + +session.write(server_handle, item_handle, value, user_id).await?; +``` + +The CLI exposes the same command as `advise-supervisory`, and `write` / +`write2` take `--user-id`. + +### Array writes replace the whole array + +A write to an array attribute **replaces the entire array**; it is not an +element-wise patch. To change a subset of elements, send the full array with +the unchanged elements included. For example, to change 2 elements of a +20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus +the 2 new ones). Sending only the 2 changed values overwrites the attribute +with a 2-element array. + ## Galaxy Repository browse The Galaxy Repository service exposes a read-only browse over the AVEVA System diff --git a/clients/rust/crates/mxgw-cli/src/main.rs b/clients/rust/crates/mxgw-cli/src/main.rs index 1ea2442..60d7b21 100644 --- a/clients/rust/crates/mxgw-cli/src/main.rs +++ b/clients/rust/crates/mxgw-cli/src/main.rs @@ -21,10 +21,11 @@ use serde_json::Value; use zb_mom_ww_mxgateway_client::galaxy::{BrowseChildrenOptions, LazyBrowseNode}; use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::DeployEvent; use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{ - alarm_feed_message, AcknowledgeAlarmRequest, AlarmFeedMessage, CloseSessionRequest, MxCommand, - MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily, MxValue as ProtoMxValue, - OpenSessionRequest, PingCommand, StreamAlarmsRequest, StreamEventsRequest, Write2BulkEntry, - WriteBulkEntry, WriteSecured2BulkEntry, WriteSecuredBulkEntry, + alarm_feed_message, AcknowledgeAlarmRequest, AdviseSupervisoryCommand, AlarmFeedMessage, + CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily, + MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamAlarmsRequest, + StreamEventsRequest, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry, + WriteSecuredBulkEntry, }; use zb_mom_ww_mxgateway_client::{ next_correlation_id, ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, @@ -105,6 +106,18 @@ enum Command { #[arg(long)] json: bool, }, + AdviseSupervisory { + #[command(flatten)] + connection: ConnectionArgs, + #[arg(long)] + session_id: String, + #[arg(long)] + server_handle: i32, + #[arg(long)] + item_handle: i32, + #[arg(long)] + json: bool, + }, SubscribeBulk { #[command(flatten)] connection: ConnectionArgs, @@ -647,6 +660,27 @@ async fn dispatch(command: Command) -> Result<(), Error> { session.advise(server_handle, item_handle).await?; print_ok("advise", json); } + Command::AdviseSupervisory { + connection, + session_id, + server_handle, + item_handle, + json, + } => { + let session = session_for(connection, session_id).await?; + session + .invoke( + MxCommandKind::AdviseSupervisory, + zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::mx_command::Payload::AdviseSupervisory( + AdviseSupervisoryCommand { + server_handle, + item_handle, + }, + ), + ) + .await?; + print_ok("advise-supervisory", json); + } Command::SubscribeBulk { connection, session_id, diff --git a/gateway.md b/gateway.md index 356a5ef..894885f 100644 --- a/gateway.md +++ b/gateway.md @@ -1049,6 +1049,15 @@ Known important parity areas from existing captures: - Invalid handles and cross-server handles have specific exception/status behavior. - STA message pumping is required for event delivery. +- A plain `Write`/`Write2` only honors its `user_id` when the item has an active + supervisory advise. Callers that do not go through the + `AuthenticateUser` → `WriteSecured`/`WriteSecured2` path must send + `AdviseSupervisory` for the item before a user id on a plain write is + recorded; otherwise the user id is ignored. +- Writing an array attribute replaces the whole array — it is not an + element-wise patch. To change a subset of elements the caller must send the + full array (unchanged elements included); sending only the changed elements + resizes the attribute. The gateway should not "fix" these behaviors unless the client explicitly opts into a non-parity mode.