diff --git a/clients/rust/Cargo.toml b/clients/rust/Cargo.toml index b4656b9..dd544fb 100644 --- a/clients/rust/Cargo.toml +++ b/clients/rust/Cargo.toml @@ -13,6 +13,11 @@ keywords = ["mxaccess", "mxgateway", "grpc", "client", "archestra"] categories = ["api-bindings", "asynchronous"] publish = ["dohertj2-gitea"] build = "build.rs" +# Ship the vendored `.proto` files so `build.rs` can regenerate the tonic/prost +# bindings from the packaged tarball — a consumer building the published crate +# has no access to the in-repo Contracts protos. See CLI-02. Cargo.toml, the +# README, and the license are included automatically. +include = ["src/**/*.rs", "protos/*.proto", "build.rs", "README.md"] [workspace] members = ["crates/mxgw-cli"] diff --git a/clients/rust/build.rs b/clients/rust/build.rs index 27843f1..bea209e 100644 --- a/clients/rust/build.rs +++ b/clients/rust/build.rs @@ -6,11 +6,25 @@ fn main() -> Result<(), Box> { configure_protoc(); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - let repo_root = manifest_dir + + // Resolve the proto source directory. In-repo builds prefer the canonical + // Contracts protos (two levels above clients/rust) so local `.proto` edits + // are picked up live. When that path is absent — e.g. a consumer building + // the crate unpacked from a published tarball — fall back to the vendored + // copies shipped in `clients/rust/protos/` (see build.rs staleness note and + // CLI-02 in archreview/remediation/50-clients.md). Vendored copies are + // build inputs only; the canonical source remains in Contracts and must be + // refreshed here whenever the `.proto` files change. + let repo_proto_root = manifest_dir .parent() .and_then(Path::parent) - .ok_or("clients/rust must live two levels below the repository root")?; - let proto_root = repo_root.join("src/ZB.MOM.WW.MxGateway.Contracts/Protos"); + .map(|repo_root| repo_root.join("src/ZB.MOM.WW.MxGateway.Contracts/Protos")); + let vendored_proto_root = manifest_dir.join("protos"); + let proto_root = match repo_proto_root { + Some(path) if path.is_dir() => path, + _ => vendored_proto_root, + }; + let gateway_proto = proto_root.join("mxaccess_gateway.proto"); let worker_proto = proto_root.join("mxaccess_worker.proto"); let galaxy_proto = proto_root.join("galaxy_repository.proto"); diff --git a/clients/rust/protos/galaxy_repository.proto b/clients/rust/protos/galaxy_repository.proto new file mode 100644 index 0000000..7b4a171 --- /dev/null +++ b/clients/rust/protos/galaxy_repository.proto @@ -0,0 +1,190 @@ +syntax = "proto3"; + +package galaxy_repository.v1; + +option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves +// additively only. Never renumber or repurpose an existing field number or +// enum value. When a field or enum value is removed, add a `reserved` range +// (and `reserved` name) covering it in the same change so a future editor +// cannot accidentally reuse the retired tag. There are no `reserved` +// declarations today because no field or enum value has ever been removed. + +// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL +// database). Lets clients enumerate the deployed object hierarchy and each +// object's dynamic attributes so they know what tag references to subscribe +// to via the MxAccessGateway service. +service GalaxyRepository { + rpc TestConnection(TestConnectionRequest) returns (TestConnectionReply); + rpc GetLastDeployTime(GetLastDeployTimeRequest) returns (GetLastDeployTimeReply); + rpc DiscoverHierarchy(DiscoverHierarchyRequest) returns (DiscoverHierarchyReply); + + // Server-stream of deploy events. The server emits the current state immediately + // on subscribe (so clients can bootstrap their cache without waiting for the next + // deploy), then emits one event each time the gateway's hierarchy cache observes + // a new galaxy.time_of_last_deploy. The sequence field is monotonically + // increasing per server start; gaps indicate the per-subscriber buffer dropped + // older events because the client was too slow. + rpc WatchDeployEvents(WatchDeployEventsRequest) returns (stream DeployEvent); + + // Returns the direct children of a parent object (or the root objects when + // `parent` is unset). Designed for OPC UA-style lazy expand: clients walk + // one level at a time instead of paging the full hierarchy. Filters mirror + // DiscoverHierarchy exactly. Backed by the same shared hierarchy cache. + rpc BrowseChildren(BrowseChildrenRequest) returns (BrowseChildrenReply); +} + +message TestConnectionRequest {} + +message TestConnectionReply { + bool ok = 1; +} + +message GetLastDeployTimeRequest {} + +message GetLastDeployTimeReply { + bool present = 1; + google.protobuf.Timestamp time_of_last_deploy = 2; +} + +message DiscoverHierarchyRequest { + // Maximum number of objects to return. The server applies its default when + // unset and rejects non-positive values. + int32 page_size = 1; + // Opaque token returned by a previous DiscoverHierarchy response. + string page_token = 2; + // Optional. When set, return only this object and its descendants. + // Empty = full hierarchy. + oneof root { + int32 root_gobject_id = 3; + string root_tag_name = 4; + string root_contained_path = 5; + } + // Optional. Cap on descendant depth from root. Zero returns only the root. + // Unset means unlimited depth. + google.protobuf.Int32Value max_depth = 6; + // Optional object category id filters. + repeated int32 category_ids = 7; + // Optional case-insensitive substring filters against template names. + repeated string template_chain_contains = 8; + // Optional anchored, case-insensitive glob over object tag_name. + string tag_name_glob = 9; + // Optional. Unset or true includes attributes. False returns object skeletons. + optional bool include_attributes = 10; + // Optional. Return only objects with at least one alarm-bearing attribute. + bool alarm_bearing_only = 11; + // Optional. Return only objects with at least one historized attribute. + bool historized_only = 12; +} + +message DiscoverHierarchyReply { + repeated GalaxyObject objects = 1; + // Non-empty when another page is available. + string next_page_token = 2; + // Total number of objects in the cached hierarchy at the time of the call. + int32 total_object_count = 3; +} + +message WatchDeployEventsRequest { + // Optional. When set, the bootstrap event is suppressed if the cached deploy + // time matches this value. Future events are still emitted normally. + google.protobuf.Timestamp last_seen_deploy_time = 1; +} + +message DeployEvent { + // Monotonically increasing per server start. Gaps indicate dropped events. + uint64 sequence = 1; + // Server wall-clock when the cache observed the deploy. + google.protobuf.Timestamp observed_at = 2; + // Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null. + google.protobuf.Timestamp time_of_last_deploy = 3; + bool time_of_last_deploy_present = 4; + int32 object_count = 5; + int32 attribute_count = 6; +} + +message GalaxyObject { + int32 gobject_id = 1; + string tag_name = 2; + string contained_name = 3; + string browse_name = 4; + int32 parent_gobject_id = 5; + bool is_area = 6; + int32 category_id = 7; + int32 hosted_by_gobject_id = 8; + repeated string template_chain = 9; + repeated GalaxyAttribute attributes = 10; +} + +message GalaxyAttribute { + string attribute_name = 1; + string full_tag_reference = 2; + // Raw Galaxy SQL `dbo.data_type` identifier, passed through unchanged. + // This is NOT a member of `mxaccess_gateway.v1.MxDataType` — Galaxy's + // type enumeration is distinct from MXAccess's wire data-type enum and + // the two must not be cast or compared. The GalaxyRepository service is + // metadata-only and deliberately does not share types with + // mxaccess_gateway.proto. See docs/GalaxyRepository.md. + int32 mx_data_type = 3; + // Human-readable name from Galaxy's `dbo.data_type` table (e.g. "Float", + // "Integer", "Boolean"). Free-form Galaxy text; not a stable enum. + string data_type_name = 4; + bool is_array = 5; + int32 array_dimension = 6; + bool array_dimension_present = 7; + // Raw Galaxy SQL attribute-category identifier, passed through unchanged. + // Galaxy-specific; not mapped to any gateway enum. See + // docs/GalaxyRepository.md. + int32 mx_attribute_category = 8; + // Raw Galaxy SQL security-classification identifier, passed through + // unchanged. Galaxy-specific; not mapped to any gateway enum. See + // docs/GalaxyRepository.md. + int32 security_classification = 9; + bool is_historized = 10; + bool is_alarm = 11; +} + +message BrowseChildrenRequest { + // Parent selector. Empty oneof returns root objects (parent_gobject_id == 0). + oneof parent { + int32 parent_gobject_id = 1; + string parent_tag_name = 2; + string parent_contained_path = 3; + } + + // Maximum number of direct children to return. Server default 500; cap 5000. + int32 page_size = 4; + // Opaque token returned by a previous BrowseChildren response. Bound to the + // cache sequence, parent selector, and the filter set; a mismatch returns + // InvalidArgument. + string page_token = 5; + + // --- Filter parity with DiscoverHierarchy. AND-combined. --- + repeated int32 category_ids = 6; + repeated string template_chain_contains = 7; + string tag_name_glob = 8; + optional bool include_attributes = 9; + bool alarm_bearing_only = 10; + bool historized_only = 11; +} + +message BrowseChildrenReply { + // Direct children matching the filter, sorted areas-first then by + // case-insensitive display name (same order as the dashboard tree). + repeated GalaxyObject children = 1; + // Non-empty when another page of siblings is available. + string next_page_token = 2; + // Total matching direct children of the parent (post-filter). + int32 total_child_count = 3; + // Parallel array, indexed with `children`. True when the child has at least + // one matching descendant under the same filter set. Lets a UI choose + // whether to draw an expand triangle without an extra round trip. + repeated bool child_has_children = 4; + // Cache sequence this reply was projected from. Clients may pass it back as + // part of the page_token contract. Mismatch on the next page -> InvalidArgument. + uint64 cache_sequence = 5; +} diff --git a/clients/rust/protos/mxaccess_gateway.proto b/clients/rust/protos/mxaccess_gateway.proto new file mode 100644 index 0000000..d3e3a53 --- /dev/null +++ b/clients/rust/protos/mxaccess_gateway.proto @@ -0,0 +1,1165 @@ +syntax = "proto3"; + +package mxaccess_gateway.v1; + +option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves +// additively only. Never renumber or repurpose an existing field number or +// enum value. When a field or enum value is removed, add a `reserved` range +// (and `reserved` name) covering it in the same change so a future editor +// cannot accidentally reuse the retired tag. + +// Public client API for MXAccess sessions hosted by the gateway. +service MxAccessGateway { + rpc OpenSession(OpenSessionRequest) returns (OpenSessionReply); + rpc CloseSession(CloseSessionRequest) returns (CloseSessionReply); + rpc Invoke(MxCommandRequest) returns (MxCommandReply); + rpc StreamEvents(StreamEventsRequest) returns (stream MxEvent); + rpc AcknowledgeAlarm(AcknowledgeAlarmRequest) returns (AcknowledgeAlarmReply); + // Session-less central alarm feed. The stream opens with the current + // active-alarm snapshot (one `active_alarm` per alarm), then a single + // `snapshot_complete`, then a `transition` for every subsequent change. + // Served by the gateway's always-on alarm monitor; any number of clients + // fan out from the single monitor without opening a worker session. + rpc StreamAlarms(StreamAlarmsRequest) returns (stream AlarmFeedMessage); + // Point-in-time snapshot of the currently-active alarm set served from the + // gateway's always-on alarm monitor cache (session-less). Used after a + // reconnect to seed Part 9 client state, or to reconcile alarms that may + // have been missed during a transport blip. Streamed so callers can + // begin processing without buffering the full set. + // `QueryActiveAlarmsRequest.alarm_filter_prefix` optionally narrows the + // snapshot to alarms whose `alarm_full_reference` starts with the given + // prefix; an empty prefix returns the full set. + rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns (stream ActiveAlarmSnapshot); +} + +// Public request shape for QueryActiveAlarms. +// Clients may leave `session_id` empty; the gateway currently ignores it and +// serves the session-less central-monitor cache. A future version may use it +// to scope the snapshot to one session. +message QueryActiveAlarmsRequest { + string session_id = 1; + string client_correlation_id = 2; + // Optional filter: when non-empty, only snapshots whose alarm_full_reference + // starts with this string are returned. + string alarm_filter_prefix = 3; +} + +message OpenSessionRequest { + string requested_backend = 1; + string client_session_name = 2; + string client_correlation_id = 3; + google.protobuf.Duration command_timeout = 4; +} + +message OpenSessionReply { + string session_id = 1; + string backend_name = 2; + int32 worker_process_id = 3; + uint32 worker_protocol_version = 4; + repeated string capabilities = 5; + google.protobuf.Duration default_command_timeout = 6; + ProtocolStatus protocol_status = 7; + // Public gateway contract version implemented by this endpoint. Clients use + // this value to reject incompatible generated-code inputs before issuing + // command-specific MXAccess calls. + uint32 gateway_protocol_version = 8; +} + +message CloseSessionRequest { + string session_id = 1; + string client_correlation_id = 2; +} + +message CloseSessionReply { + string session_id = 1; + SessionState final_state = 2; + ProtocolStatus protocol_status = 3; +} + +message StreamEventsRequest { + string session_id = 1; + uint64 after_worker_sequence = 2; +} + +message MxCommandRequest { + string session_id = 1; + string client_correlation_id = 2; + MxCommand command = 3; +} + +message MxCommand { + MxCommandKind kind = 1; + + oneof payload { + RegisterCommand register = 10; + UnregisterCommand unregister = 11; + AddItemCommand add_item = 12; + AddItem2Command add_item2 = 13; + RemoveItemCommand remove_item = 14; + AdviseCommand advise = 15; + UnAdviseCommand un_advise = 16; + AdviseSupervisoryCommand advise_supervisory = 17; + AddBufferedItemCommand add_buffered_item = 18; + SetBufferedUpdateIntervalCommand set_buffered_update_interval = 19; + SuspendCommand suspend = 20; + ActivateCommand activate = 21; + WriteCommand write = 22; + Write2Command write2 = 23; + WriteSecuredCommand write_secured = 24; + WriteSecured2Command write_secured2 = 25; + AuthenticateUserCommand authenticate_user = 26; + ArchestrAUserToIdCommand archestra_user_to_id = 27; + AddItemBulkCommand add_item_bulk = 28; + AdviseItemBulkCommand advise_item_bulk = 29; + RemoveItemBulkCommand remove_item_bulk = 30; + UnAdviseItemBulkCommand un_advise_item_bulk = 31; + SubscribeBulkCommand subscribe_bulk = 32; + UnsubscribeBulkCommand unsubscribe_bulk = 33; + SubscribeAlarmsCommand subscribe_alarms = 34; + UnsubscribeAlarmsCommand unsubscribe_alarms = 35; + AcknowledgeAlarmCommand acknowledge_alarm_command = 36; + QueryActiveAlarmsCommand query_active_alarms_command = 37; + AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38; + WriteBulkCommand write_bulk = 39; + Write2BulkCommand write2_bulk = 40; + WriteSecuredBulkCommand write_secured_bulk = 41; + WriteSecured2BulkCommand write_secured2_bulk = 42; + ReadBulkCommand read_bulk = 43; + PingCommand ping = 100; + GetSessionStateCommand get_session_state = 101; + GetWorkerInfoCommand get_worker_info = 102; + DrainEventsCommand drain_events = 103; + ShutdownWorkerCommand shutdown_worker = 104; + } +} + +enum MxCommandKind { + MX_COMMAND_KIND_UNSPECIFIED = 0; + MX_COMMAND_KIND_REGISTER = 1; + MX_COMMAND_KIND_UNREGISTER = 2; + MX_COMMAND_KIND_ADD_ITEM = 3; + MX_COMMAND_KIND_ADD_ITEM2 = 4; + MX_COMMAND_KIND_REMOVE_ITEM = 5; + MX_COMMAND_KIND_ADVISE = 6; + MX_COMMAND_KIND_UN_ADVISE = 7; + MX_COMMAND_KIND_ADVISE_SUPERVISORY = 8; + MX_COMMAND_KIND_ADD_BUFFERED_ITEM = 9; + MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL = 10; + MX_COMMAND_KIND_SUSPEND = 11; + MX_COMMAND_KIND_ACTIVATE = 12; + MX_COMMAND_KIND_WRITE = 13; + MX_COMMAND_KIND_WRITE2 = 14; + MX_COMMAND_KIND_WRITE_SECURED = 15; + MX_COMMAND_KIND_WRITE_SECURED2 = 16; + MX_COMMAND_KIND_AUTHENTICATE_USER = 17; + MX_COMMAND_KIND_ARCHESTRA_USER_TO_ID = 18; + MX_COMMAND_KIND_ADD_ITEM_BULK = 19; + MX_COMMAND_KIND_ADVISE_ITEM_BULK = 20; + MX_COMMAND_KIND_REMOVE_ITEM_BULK = 21; + MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK = 22; + MX_COMMAND_KIND_SUBSCRIBE_BULK = 23; + MX_COMMAND_KIND_UNSUBSCRIBE_BULK = 24; + MX_COMMAND_KIND_SUBSCRIBE_ALARMS = 25; + MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26; + MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27; + MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28; + MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME = 29; + MX_COMMAND_KIND_WRITE_BULK = 30; + MX_COMMAND_KIND_WRITE2_BULK = 31; + MX_COMMAND_KIND_WRITE_SECURED_BULK = 32; + MX_COMMAND_KIND_WRITE_SECURED2_BULK = 33; + MX_COMMAND_KIND_READ_BULK = 34; + MX_COMMAND_KIND_PING = 100; + MX_COMMAND_KIND_GET_SESSION_STATE = 101; + MX_COMMAND_KIND_GET_WORKER_INFO = 102; + MX_COMMAND_KIND_DRAIN_EVENTS = 103; + MX_COMMAND_KIND_SHUTDOWN_WORKER = 104; +} + +message RegisterCommand { + string client_name = 1; +} + +message UnregisterCommand { + int32 server_handle = 1; +} + +message AddItemCommand { + int32 server_handle = 1; + string item_definition = 2; +} + +message AddItem2Command { + int32 server_handle = 1; + string item_definition = 2; + string item_context = 3; +} + +message RemoveItemCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message AdviseCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message UnAdviseCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message AdviseSupervisoryCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message AddBufferedItemCommand { + int32 server_handle = 1; + string item_definition = 2; + string item_context = 3; +} + +message SetBufferedUpdateIntervalCommand { + int32 server_handle = 1; + int32 update_interval_milliseconds = 2; +} + +message SuspendCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message ActivateCommand { + int32 server_handle = 1; + int32 item_handle = 2; +} + +message WriteCommand { + int32 server_handle = 1; + int32 item_handle = 2; + MxValue value = 3; + int32 user_id = 4; +} + +message Write2Command { + int32 server_handle = 1; + int32 item_handle = 2; + MxValue value = 3; + MxValue timestamp_value = 4; + int32 user_id = 5; +} + +message WriteSecuredCommand { + int32 server_handle = 1; + int32 item_handle = 2; + int32 current_user_id = 3; + int32 verifier_user_id = 4; + // Credential-sensitive write value. Implementations must not log this field + // unless an explicit redacted value-logging path is enabled. + MxValue value = 5; +} + +message WriteSecured2Command { + int32 server_handle = 1; + int32 item_handle = 2; + int32 current_user_id = 3; + int32 verifier_user_id = 4; + // Credential-sensitive write value. Implementations must not log this field + // unless an explicit redacted value-logging path is enabled. + MxValue value = 5; + MxValue timestamp_value = 6; +} + +message AuthenticateUserCommand { + int32 server_handle = 1; + string verify_user = 2; + // Raw MXAccess credential. Implementations must keep this field out of logs, + // metrics labels, command lines, and diagnostics. + string verify_user_password = 3; +} + +message ArchestrAUserToIdCommand { + int32 server_handle = 1; + string user_id_guid = 2; +} + +message AddItemBulkCommand { + int32 server_handle = 1; + repeated string tag_addresses = 2; +} + +message AdviseItemBulkCommand { + int32 server_handle = 1; + repeated int32 item_handles = 2; +} + +message RemoveItemBulkCommand { + int32 server_handle = 1; + repeated int32 item_handles = 2; +} + +message UnAdviseItemBulkCommand { + int32 server_handle = 1; + repeated int32 item_handles = 2; +} + +message SubscribeBulkCommand { + int32 server_handle = 1; + repeated string tag_addresses = 2; +} + +// Provider selection / current provider for the alarm feed. The zero value +// has two distinct meanings depending on the use site: +// - As SubscribeAlarmsCommand.forced_mode, UNSPECIFIED means auto: alarmmgr +// primary with subtag fallback. +// - As a provenance value (OnAlarmTransitionEvent.source_provider, +// ActiveAlarmSnapshot.source_provider, OnAlarmProviderModeChangedEvent.mode, +// AlarmProviderStatus.mode), the worker always emits ALARMMGR or SUBTAG and +// never UNSPECIFIED; clients should treat a UNSPECIFIED provenance value as +// "unknown / not yet determined". +enum AlarmProviderMode { + ALARM_PROVIDER_MODE_UNSPECIFIED = 0; + ALARM_PROVIDER_MODE_ALARMMGR = 1; + ALARM_PROVIDER_MODE_SUBTAG = 2; +} + +// Subscribe the worker's alarm consumer to an AVEVA alarm provider. +// Subscription expression follows the canonical +// `\\\Galaxy!` format (literal "Galaxy" provider). The +// worker spins up a wnwrapConsumer-backed subscription on its STA on +// first call; subsequent calls are an error (use UnsubscribeAlarms then +// SubscribeAlarms to reconfigure). +message SubscribeAlarmsCommand { + string subscription_expression = 1; + // UNSPECIFIED = auto-failover/failback. ALARMMGR/SUBTAG force one provider. + AlarmProviderMode forced_mode = 2; + // Subtag watch-list resolved by the gateway (GR SQL + config). Empty in pure + // alarmmgr mode; in subtag mode it bounds what the consumer can observe. + repeated AlarmSubtagTarget watch_list = 3; + AlarmFailoverConfig failover = 4; +} + +// Tear down the worker's alarm consumer. No-op if no subscription is +// currently active. +message UnsubscribeAlarmsCommand { +} + +// One alarm attribute the subtag fallback consumer advises. Addresses are full +// MXAccess item references the worker passes straight to AddItem. +message AlarmSubtagTarget { + string alarm_full_reference = 1; // e.g. "Galaxy!Area.Tank01.Level.HiHi" + string source_object_reference = 2; // e.g. "Tank01" + string active_subtag = 3; // item address of the in-alarm boolean + string acked_subtag = 4; // item address of the acknowledged boolean + string ack_comment_subtag = 5; // writable ack-comment attribute (ack write target) + string priority_subtag = 6; // optional severity source; empty if absent +} + +message AlarmFailoverConfig { + int32 consecutive_failure_threshold = 1; // wnwrap COM failures before switching (>=1) + int32 failback_probe_interval_seconds = 2; // probe cadence while degraded (>=1) + int32 failback_stable_probes = 3; // clean probes before switching back (>=1) +} + +// Acknowledge a single alarm by its GUID. Operator identity fields are +// recorded atomically with the ack transition in the alarm-history log. +// The reply's hresult / native_status surfaces AVEVA's +// AlarmAckByGUID return code. +message AcknowledgeAlarmCommand { + // Canonical 8-4-4-4-12 GUID string (e.g. "BCC47053-9542-4D65-BDAA-BCDEA6A32A73"). + string alarm_guid = 1; + string comment = 2; + string operator_user = 3; + string operator_node = 4; + string operator_domain = 5; + string operator_full_name = 6; +} + +// Snapshot the currently-active alarm set. Optional filter prefix scopes +// the snapshot to alarms whose alarm_full_reference starts with the +// supplied string (matches QueryActiveAlarmsRequest.alarm_filter_prefix). +message QueryActiveAlarmsCommand { + string alarm_filter_prefix = 1; +} + +// Acknowledge a single alarm by its (name, provider, group) tuple. Used +// when the public RPC's AlarmFullReference (Provider!Group.Tag) cannot +// be resolved to a GUID directly. The worker invokes +// wwAlarmConsumerClass.AlarmAckByName which reaches the same alarm +// history path as AlarmAckByGUID. +message AcknowledgeAlarmByNameCommand { + // Tag/alarm name (e.g. "TestMachine_001.TestAlarm001"). Tag itself + // may contain dots; the gateway-side parser splits on the first dot + // after the '!' separator. + string alarm_name = 1; + // AVEVA alarm-provider name (literal "Galaxy" for ArchestrA Galaxies). + string provider_name = 2; + // Area/group name (e.g. "TestArea"). + string group_name = 3; + string comment = 4; + string operator_user = 5; + string operator_node = 6; + string operator_domain = 7; + string operator_full_name = 8; +} + +message UnsubscribeBulkCommand { + int32 server_handle = 1; + repeated int32 item_handles = 2; +} + +// Bulk Write — sequential MXAccess Write per entry, on the worker's STA. +// MXAccess has no native bulk write; each entry round-trips through the same +// single-item Write path the gateway uses today. Per-item failures appear as +// BulkWriteResult entries with `was_successful = false` and never throw. +message WriteBulkCommand { + int32 server_handle = 1; + repeated WriteBulkEntry entries = 2; +} + +message WriteBulkEntry { + int32 item_handle = 1; + MxValue value = 2; + int32 user_id = 3; +} + +// Bulk Write2 — sequential MXAccess Write2 (timestamped) per entry. +message Write2BulkCommand { + int32 server_handle = 1; + repeated Write2BulkEntry entries = 2; +} + +message Write2BulkEntry { + int32 item_handle = 1; + MxValue value = 2; + MxValue timestamp_value = 3; + int32 user_id = 4; +} + +// Bulk WriteSecured — sequential MXAccess WriteSecured per entry. +// Credential-sensitive values (`value`) MUST be kept out of logs, metrics +// labels, command lines, and diagnostics — same redaction rules as the +// single-item WriteSecured contract. +message WriteSecuredBulkCommand { + int32 server_handle = 1; + repeated WriteSecuredBulkEntry entries = 2; +} + +message WriteSecuredBulkEntry { + int32 item_handle = 1; + int32 current_user_id = 2; + int32 verifier_user_id = 3; + // Credential-sensitive write value. Implementations must not log this field + // unless an explicit redacted value-logging path is enabled. + MxValue value = 4; +} + +// Bulk WriteSecured2 — sequential MXAccess WriteSecured2 (timestamped) per +// entry. Same redaction rules apply. +message WriteSecured2BulkCommand { + int32 server_handle = 1; + repeated WriteSecured2BulkEntry entries = 2; +} + +message WriteSecured2BulkEntry { + int32 item_handle = 1; + int32 current_user_id = 2; + int32 verifier_user_id = 3; + // Credential-sensitive write value. Implementations must not log this field + // unless an explicit redacted value-logging path is enabled. + MxValue value = 4; + MxValue timestamp_value = 5; +} + +// Bulk Read — snapshot the current value for each requested tag. MXAccess COM +// has no synchronous Read; the worker implements ReadBulk as: +// +// - If the tag is already in the session's item registry AND that item is +// currently advised AND the worker has a cached OnDataChange for it, the +// reply returns the cached value WITHOUT modifying the existing +// subscription (was_cached = true). +// - Otherwise the worker takes the snapshot lifecycle itself: AddItem + +// Advise, wait up to `timeout_ms` for the first OnDataChange, then +// UnAdvise + RemoveItem before returning. The session is left exactly +// as it was before the call (was_cached = false). +// +// `timeout_ms == 0` uses the gateway-configured default (1000 ms). +message ReadBulkCommand { + int32 server_handle = 1; + repeated string tag_addresses = 2; + uint32 timeout_ms = 3; +} + +message PingCommand { + string message = 1; +} + +message GetSessionStateCommand { +} + +message GetWorkerInfoCommand { +} + +message DrainEventsCommand { + uint32 max_events = 1; +} + +message ShutdownWorkerCommand { + google.protobuf.Duration grace_period = 1; +} + +message MxCommandReply { + string session_id = 1; + string correlation_id = 2; + MxCommandKind kind = 3; + ProtocolStatus protocol_status = 4; + // HRESULT captured from MXAccess or a COM exception. This remains separate + // from gateway protocol status so MXAccess parity details are not hidden by + // transport failures. + optional int32 hresult = 5; + MxValue return_value = 6; + repeated MxStatusProxy statuses = 7; + string diagnostic_message = 8; + + oneof payload { + RegisterReply register = 20; + AddItemReply add_item = 21; + AddItem2Reply add_item2 = 22; + AddBufferedItemReply add_buffered_item = 23; + SuspendReply suspend = 24; + ActivateReply activate = 25; + AuthenticateUserReply authenticate_user = 26; + ArchestrAUserToIdReply archestra_user_to_id = 27; + BulkSubscribeReply add_item_bulk = 28; + BulkSubscribeReply advise_item_bulk = 29; + BulkSubscribeReply remove_item_bulk = 30; + BulkSubscribeReply un_advise_item_bulk = 31; + BulkSubscribeReply subscribe_bulk = 32; + BulkSubscribeReply unsubscribe_bulk = 33; + // Reply payload for BOTH MX_COMMAND_KIND_ACKNOWLEDGE_ALARM (by GUID) + // and MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME. There is intentionally + // no by-name-specific reply case: the by-name ack carries no outcome + // detail beyond the native ack return code, so the worker reuses this + // `acknowledge_alarm` payload for both command kinds (the worker's + // MxAccessCommandExecutor sets `acknowledge_alarm` for the by-name arm + // too). Consumers must dispatch on MxCommandReply.kind, not on the + // payload case, to tell the two acks apart. The top-level `hresult` + // mirrors AcknowledgeAlarmReplyPayload.native_status and is preferred. + AcknowledgeAlarmReplyPayload acknowledge_alarm = 34; + QueryActiveAlarmsReplyPayload query_active_alarms = 35; + BulkWriteReply write_bulk = 36; + BulkWriteReply write2_bulk = 37; + BulkWriteReply write_secured_bulk = 38; + BulkWriteReply write_secured2_bulk = 39; + BulkReadReply read_bulk = 40; + SessionStateReply session_state = 100; + WorkerInfoReply worker_info = 101; + DrainEventsReply drain_events = 102; + } +} + +message RegisterReply { + int32 server_handle = 1; +} + +message AddItemReply { + int32 item_handle = 1; +} + +message AddItem2Reply { + int32 item_handle = 1; +} + +message AddBufferedItemReply { + int32 item_handle = 1; +} + +message SuspendReply { + MxStatusProxy status = 1; +} + +message ActivateReply { + MxStatusProxy status = 1; +} + +message AuthenticateUserReply { + int32 user_id = 1; +} + +message ArchestrAUserToIdReply { + int32 user_id = 1; +} + +message SubscribeResult { + int32 server_handle = 1; + string tag_address = 2; + int32 item_handle = 3; + bool was_successful = 4; + string error_message = 5; +} + +message BulkSubscribeReply { + repeated SubscribeResult results = 1; +} + +// Per-item result for the four bulk write families. `item_handle` mirrors the +// request entry's item_handle so callers can correlate inputs to outputs even +// when the gateway's per-entry `IConstraintEnforcer.CheckWriteHandleAsync` +// filter (see `MxAccessGatewayService.ReplaceWriteBulkEntries` and +// `docs/Authorization.md`) dropped some entries before reaching the worker. +// Per-item failures populate `error_message` + `hresult` and never raise — +// callers iterate and inspect each entry. +message BulkWriteResult { + int32 server_handle = 1; + int32 item_handle = 2; + bool was_successful = 3; + optional int32 hresult = 4; + repeated MxStatusProxy statuses = 5; + string error_message = 6; +} + +message BulkWriteReply { + repeated BulkWriteResult results = 1; +} + +// Per-tag result for ReadBulk. `was_cached` is true when the value came from +// an existing live subscription's last OnDataChange (the worker did not touch +// the subscription); false when the worker took the AddItem + Advise + wait + +// UnAdvise + RemoveItem snapshot lifecycle itself. +// +// On `was_successful = true`, `value`, `quality`, `source_timestamp`, and +// `statuses` carry the read data (from the cached subscription or the snapshot +// lifecycle, depending on `was_cached`) and `error_message` is empty. On +// `was_successful = false`, only `server_handle`, `tag_address`, `item_handle` +// (when allocated), `was_cached`, and `error_message` are populated; `value`, +// `quality`, `source_timestamp`, and `statuses` are left at their proto3 +// defaults (null / 0 / null / empty) and must not be read as data — they are +// wire-indistinguishable from "value is null with quality bad" data and serve +// only as absent markers. ReadBulk has no `hresult` field by design (its +// outcomes are timeout / cache / lifecycle states, not MXAccess COM return +// codes — see `docs/DesignDecisions.md` "Bulk Command Family"). Per-tag +// failures populate `error_message` and never raise — callers iterate and +// inspect each entry. +message BulkReadResult { + int32 server_handle = 1; + string tag_address = 2; + int32 item_handle = 3; + bool was_successful = 4; + bool was_cached = 5; + MxValue value = 6; + int32 quality = 7; + google.protobuf.Timestamp source_timestamp = 8; + repeated MxStatusProxy statuses = 9; + string error_message = 10; +} + +message BulkReadReply { + repeated BulkReadResult results = 1; +} + +message SessionStateReply { + SessionState state = 1; +} + +message WorkerInfoReply { + int32 worker_process_id = 1; + string worker_version = 2; + string mxaccess_progid = 3; + string mxaccess_clsid = 4; +} + +message DrainEventsReply { + repeated MxEvent events = 1; +} + +// Reply payload for AcknowledgeAlarmCommand AND +// AcknowledgeAlarmByNameCommand — both ack command kinds reuse this +// payload case (`MxCommandReply.acknowledge_alarm`); there is no +// dedicated by-name reply case. Surfaces AVEVA's native ack return +// code (AlarmAckByGUID for the GUID arm, AlarmAckByName for the +// by-name arm); 0 means success. The MxCommandReply's hresult field +// carries the same value and is preferred for protocol consumers — +// this payload exists so the gateway-side WorkerAlarmRpcDispatcher +// can echo native_status into AcknowledgeAlarmReply.hresult without +// unpacking the outer envelope. +message AcknowledgeAlarmReplyPayload { + int32 native_status = 1; +} + +// Reply payload for QueryActiveAlarmsCommand. The worker walks +// IMxAccessAlarmConsumer.SnapshotActiveAlarms and packs each record as +// an ActiveAlarmSnapshot proto for the gateway-side ConditionRefresh +// stream. +message QueryActiveAlarmsReplyPayload { + repeated ActiveAlarmSnapshot snapshots = 1; +} + +message MxEvent { + MxEventFamily family = 1; + string session_id = 2; + int32 server_handle = 3; + int32 item_handle = 4; + MxValue value = 5; + int32 quality = 6; + google.protobuf.Timestamp source_timestamp = 7; + repeated MxStatusProxy statuses = 8; + uint64 worker_sequence = 9; + google.protobuf.Timestamp worker_timestamp = 10; + google.protobuf.Timestamp gateway_receive_timestamp = 11; + optional int32 hresult = 12; + string raw_status = 13; + // Gateway-synthesized reconnect-replay gap signal. Set ONLY on the single + // sentinel MxEvent the gateway emits at the head of a StreamEvents stream + // that was resumed via StreamEventsRequest.after_worker_sequence when the + // requested sequence is older than the oldest event still retained in the + // session replay ring (i.e. events were evicted and cannot be replayed). + // On that sentinel, `family` is UNSPECIFIED, the `body` oneof is unset, and + // no per-item fields (server_handle/item_handle/value/...) are populated; + // clients MUST treat a present `replay_gap` as "you missed events — discard + // local state and re-snapshot" and read `requested_after_sequence` / + // `oldest_available_sequence` from it. Unset on every normal MXAccess event. + // This field is ONLY ever set on events returned from the StreamEvents server + // stream; it is ALWAYS unset on events in DrainEventsReply (the diagnostic + // drain path never emits the sentinel). + // Additive (proto3): existing clients that ignore this field continue to + // deserialize the stream unchanged. + optional ReplayGap replay_gap = 14; + + oneof body { + OnDataChangeEvent on_data_change = 20; + OnWriteCompleteEvent on_write_complete = 21; + OperationCompleteEvent operation_complete = 22; + OnBufferedDataChangeEvent on_buffered_data_change = 23; + OnAlarmTransitionEvent on_alarm_transition = 24; + OnAlarmProviderModeChangedEvent on_alarm_provider_mode_changed = 25; + } +} + +// Reconnect-replay gap signal carried by a sentinel MxEvent (MxEvent.replay_gap) +// when a client resumes StreamEvents via after_worker_sequence but the requested +// sequence predates the oldest event still held in the session replay ring. +// The events in the open interval (requested_after_sequence, oldest_available_sequence) +// were evicted from the ring and cannot be replayed, so the client must +// re-snapshot rather than assume a contiguous event history. +message ReplayGap { + // The worker_sequence the client asked to resume after + // (StreamEventsRequest.after_worker_sequence). + uint64 requested_after_sequence = 1; + // The oldest worker_sequence still retained in the replay ring and available + // for replay. Events with worker_sequence in the open interval + // (requested_after_sequence, oldest_available_sequence) were evicted and are + // unrecoverable. oldest_available_sequence itself IS still retained: a client + // that wishes to resume without incurring another gap MUST set + // after_worker_sequence = oldest_available_sequence - 1 in the next + // StreamEventsRequest, which will cause the server to replay starting at + // oldest_available_sequence (the first retained event). + uint64 oldest_available_sequence = 2; +} + +enum MxEventFamily { + MX_EVENT_FAMILY_UNSPECIFIED = 0; + MX_EVENT_FAMILY_ON_DATA_CHANGE = 1; + MX_EVENT_FAMILY_ON_WRITE_COMPLETE = 2; + MX_EVENT_FAMILY_OPERATION_COMPLETE = 3; + MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE = 4; + MX_EVENT_FAMILY_ON_ALARM_TRANSITION = 5; + MX_EVENT_FAMILY_ON_ALARM_PROVIDER_MODE_CHANGED = 6; +} + +message OnDataChangeEvent { +} + +message OnWriteCompleteEvent { +} + +message OperationCompleteEvent { +} + +message OnBufferedDataChangeEvent { + MxDataType data_type = 1; + MxArray quality_values = 2; + MxArray timestamp_values = 3; + int32 raw_data_type = 4; +} + +// Carries a single MXAccess alarm transition (raise / acknowledge / clear / +// re-trigger) in native MXAccess terms. The Part 9 state machine + ACL + +// multi-source aggregation lives in lmxopcua's AlarmConditionService; the +// gateway is UA-agnostic and forwards the raw payload. +message OnAlarmTransitionEvent { + // Fully-qualified alarm reference (e.g. "Tank01.Level.HiHi"). Stable across + // transitions of the same condition; used by the lmxopcua side to correlate + // raise/ack/clear into a single Part 9 condition. + string alarm_full_reference = 1; + + // Galaxy-side source object reference (e.g. "Tank01"). Empty for alarms + // that do not bind to a Galaxy object. + string source_object_reference = 2; + + // MxAccess alarm-type qualifier (e.g. "AnalogLimitAlarm.HiHi", "DiscAlarm"). + string alarm_type_name = 3; + + // What kind of state change this event represents. + AlarmTransitionKind transition_kind = 4; + + // Raw MXAccess severity value. Mapping to OPC UA 0-1000 happens server-side + // in lmxopcua via MxAccessSeverityMapper; the gateway preserves the native + // MXAccess scale. + int32 severity = 5; + + // When the alarm originally entered the active state. Preserved across + // acknowledge transitions so the Part 9 condition keeps the original raise + // time. Unset on retrigger from a previously-cleared condition. + google.protobuf.Timestamp original_raise_timestamp = 6; + + // When this specific transition occurred (raise time on Raise, ack time on + // Acknowledge, clear time on Clear). + google.protobuf.Timestamp transition_timestamp = 7; + + // Operator principal recorded by MXAccess on Acknowledge transitions. + // Empty on raise / clear. + string operator_user = 8; + + // Operator-supplied comment recorded by MXAccess on Acknowledge transitions. + // Empty on raise / clear or when no comment was supplied. + string operator_comment = 9; + + // MxAccess alarm category (taxonomy bucket configured in the Galaxy + // template, e.g. "Process", "Safety", "Diagnostics"). + string category = 10; + + // Human-readable alarm description from the MxAccess alarm definition. + string description = 11; + + // Current alarm value (the value of the source attribute at the moment of + // transition). Optional; populated when MxAccess surfaces it. + MxValue current_value = 12; + + // Limit/threshold value that triggered the transition for limit alarms. + // Optional; populated for AnalogLimitAlarm-family transitions. + MxValue limit_value = 13; + + // True when this transition came from the subtag-monitoring fallback rather + // than the native alarmmgr provider — synthesized from data changes, reduced + // fidelity (synthetic GUID, no native raise time). + bool degraded = 14; + // Which provider produced this transition. + AlarmProviderMode source_provider = 15; +} + +message OnAlarmProviderModeChangedEvent { + AlarmProviderMode mode = 1; + string reason = 2; + int32 hresult = 3; // COM HRESULT that triggered failover; 0 on failback + google.protobuf.Timestamp at = 4; +} + +enum AlarmTransitionKind { + ALARM_TRANSITION_KIND_UNSPECIFIED = 0; + ALARM_TRANSITION_KIND_RAISE = 1; + ALARM_TRANSITION_KIND_ACKNOWLEDGE = 2; + ALARM_TRANSITION_KIND_CLEAR = 3; + ALARM_TRANSITION_KIND_RETRIGGER = 4; +} + +// Snapshot of a currently-active MXAccess alarm condition, returned from a +// QueryActiveAlarms ConditionRefresh stream. +message ActiveAlarmSnapshot { + string alarm_full_reference = 1; + string source_object_reference = 2; + string alarm_type_name = 3; + int32 severity = 4; + google.protobuf.Timestamp original_raise_timestamp = 5; + AlarmConditionState current_state = 6; + string category = 7; + string description = 8; + // When the most recent state transition occurred (last raise, last ack, + // last clear). + google.protobuf.Timestamp last_transition_timestamp = 9; + // Operator who acknowledged the alarm if the current state is ActiveAcked. + // Empty otherwise. + string operator_user = 10; + // Operator comment recorded with the most recent acknowledge if the current + // state is ActiveAcked. Empty otherwise. + string operator_comment = 11; + MxValue current_value = 12; + MxValue limit_value = 13; + // True when this snapshot came from the subtag-monitoring fallback rather + // than the native alarmmgr provider — synthesized from data changes, reduced + // fidelity (synthetic GUID, no native raise time). Mirrors + // OnAlarmTransitionEvent.degraded. + bool degraded = 14; + // Which provider produced this snapshot. Mirrors + // OnAlarmTransitionEvent.source_provider; always ALARMMGR or SUBTAG on the + // wire (never UNSPECIFIED). + AlarmProviderMode source_provider = 15; +} + +enum AlarmConditionState { + ALARM_CONDITION_STATE_UNSPECIFIED = 0; + ALARM_CONDITION_STATE_ACTIVE = 1; + ALARM_CONDITION_STATE_ACTIVE_ACKED = 2; + ALARM_CONDITION_STATE_INACTIVE = 3; +} + +message AcknowledgeAlarmRequest { + // Retired: acknowledgement is session-less — it routes to the gateway's + // central alarm monitor, not a client worker session. + reserved 1; + reserved "session_id"; + string client_correlation_id = 2; + // Fully-qualified alarm reference matching OnAlarmTransitionEvent.alarm_full_reference. + string alarm_full_reference = 3; + // Operator-supplied comment forwarded to MXAccess. + string comment = 4; + // Operator principal performing the acknowledgement. The lmxopcua side + // resolves this from the OPC UA session prior to invoking the RPC. + string operator_user = 5; +} + +message AcknowledgeAlarmReply { + // Retired: see AcknowledgeAlarmRequest — acknowledgement is session-less. + reserved 1; + reserved "session_id"; + string correlation_id = 2; + ProtocolStatus protocol_status = 3; + // Native ack return code echoed from the worker. The worker carries the + // ack outcome as a single int32 (AcknowledgeAlarmReplyPayload.native_status, + // = AlarmAckByName / AlarmAckByGUID return code; 0 = success); the gateway's + // WorkerAlarmRpcDispatcher copies that value here. This is the authoritative + // ack-outcome field for the public RPC. Absent only when the worker reply + // omitted the value entirely (a protocol violation). + optional int32 hresult = 4; + // Reserved for a structured MxStatusProxy view of the ack outcome. The + // worker by-name/by-GUID ack path produces only the int32 return code + // (see `hresult`), so the current gateway leaves this field UNSET on every + // reply. Clients must read `hresult` (and `protocol_status`) for the ack + // result and must not depend on `status` being populated. + MxStatusProxy status = 5; + string diagnostic_message = 6; +} + +// Request to attach to the gateway's central alarm feed (StreamAlarms). +message StreamAlarmsRequest { + string client_correlation_id = 1; + // Optional alarm-reference prefix scoping the feed to an equipment + // sub-tree. Empty streams every active alarm. + string alarm_filter_prefix = 2; +} + +// One message on the StreamAlarms feed. The stream opens with one +// `active_alarm` per currently-active alarm, then a single +// `snapshot_complete`, then a `transition` for every subsequent change. +message AlarmFeedMessage { + oneof payload { + // Part of the initial active-alarm snapshot (ConditionRefresh). + ActiveAlarmSnapshot active_alarm = 1; + // Sentinel: the initial snapshot is fully delivered and `transition` + // messages follow. Always true when present. + bool snapshot_complete = 2; + // A live alarm state change (raise / acknowledge / clear). + OnAlarmTransitionEvent transition = 3; + // Provider-mode status. Emitted once on stream open and again on every + // failover/failback so late joiners learn the current mode immediately. + AlarmProviderStatus provider_status = 4; + } +} + +message AlarmProviderStatus { + AlarmProviderMode mode = 1; + bool degraded = 2; // true whenever mode == SUBTAG + string reason = 3; // human-readable switch reason + google.protobuf.Timestamp since = 4; +} + +message MxStatusProxy { + // Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct + // (a 16-bit signed value in the COM struct, widened to int32 on the + // wire). Despite the name it is NOT a boolean — it is the raw numeric + // indicator the worker reads off the COM struct without reinterpretation. + // It is carried verbatim for diagnostics; the authoritative success/ + // failure of the operation is `category` (MX_STATUS_CATEGORY_OK marks + // success), with `detail`, `diagnostic_text`, `raw_category`, and + // `raw_detected_by` describing any non-OK outcome. Clients should branch + // on `category`, not on a specific `success` value. + int32 success = 1; + MxStatusCategory category = 2; + MxStatusSource detected_by = 3; + int32 detail = 4; + int32 raw_category = 5; + int32 raw_detected_by = 6; + string diagnostic_text = 7; +} + +enum MxStatusCategory { + MX_STATUS_CATEGORY_UNSPECIFIED = 0; + MX_STATUS_CATEGORY_UNKNOWN = 1; + MX_STATUS_CATEGORY_OK = 2; + MX_STATUS_CATEGORY_PENDING = 3; + MX_STATUS_CATEGORY_WARNING = 4; + MX_STATUS_CATEGORY_COMMUNICATION_ERROR = 5; + MX_STATUS_CATEGORY_CONFIGURATION_ERROR = 6; + MX_STATUS_CATEGORY_OPERATIONAL_ERROR = 7; + MX_STATUS_CATEGORY_SECURITY_ERROR = 8; + MX_STATUS_CATEGORY_SOFTWARE_ERROR = 9; + MX_STATUS_CATEGORY_OTHER_ERROR = 10; +} + +enum MxStatusSource { + MX_STATUS_SOURCE_UNSPECIFIED = 0; + MX_STATUS_SOURCE_UNKNOWN = 1; + MX_STATUS_SOURCE_REQUESTING_LMX = 2; + MX_STATUS_SOURCE_RESPONDING_LMX = 3; + MX_STATUS_SOURCE_REQUESTING_NMX = 4; + MX_STATUS_SOURCE_RESPONDING_NMX = 5; + MX_STATUS_SOURCE_REQUESTING_AUTOMATION_OBJECT = 6; + MX_STATUS_SOURCE_RESPONDING_AUTOMATION_OBJECT = 7; +} + +message MxValue { + MxDataType data_type = 1; + string variant_type = 2; + bool is_null = 3; + string raw_diagnostic = 4; + int32 raw_data_type = 5; + + oneof kind { + bool bool_value = 10; + int32 int32_value = 11; + int64 int64_value = 12; + float float_value = 13; + double double_value = 14; + string string_value = 15; + google.protobuf.Timestamp timestamp_value = 16; + MxArray array_value = 17; + bytes raw_value = 18; + MxSparseArray sparse_array_value = 19; + } +} + +message MxArray { + MxDataType element_data_type = 1; + string variant_type = 2; + repeated uint32 dimensions = 3; + string raw_diagnostic = 4; + int32 raw_element_data_type = 5; + + oneof values { + BoolArray bool_values = 10; + Int32Array int32_values = 11; + Int64Array int64_values = 12; + FloatArray float_values = 13; + DoubleArray double_values = 14; + StringArray string_values = 15; + TimestampArray timestamp_values = 16; + RawArray raw_values = 17; + } +} + +// Write-only sparse array value. The gateway expands this into a full, +// default-filled MxArray before forwarding to the worker; the worker never +// receives or produces it. Unmentioned indices take the element type's +// default (reset, NOT preserved). +message MxSparseArray { + MxDataType element_data_type = 1; + uint32 total_length = 2; + repeated MxSparseElement elements = 3; +} + +message MxSparseElement { + uint32 index = 1; + MxValue value = 2; // scalar +} + +message BoolArray { + repeated bool values = 1; +} + +message Int32Array { + repeated int32 values = 1; +} + +message Int64Array { + repeated int64 values = 1; +} + +message FloatArray { + repeated float values = 1; +} + +message DoubleArray { + repeated double values = 1; +} + +message StringArray { + repeated string values = 1; +} + +message TimestampArray { + repeated google.protobuf.Timestamp values = 1; +} + +message RawArray { + repeated bytes values = 1; +} + +enum MxDataType { + MX_DATA_TYPE_UNSPECIFIED = 0; + MX_DATA_TYPE_UNKNOWN = 1; + MX_DATA_TYPE_NO_DATA = 2; + MX_DATA_TYPE_BOOLEAN = 3; + MX_DATA_TYPE_INTEGER = 4; + MX_DATA_TYPE_FLOAT = 5; + MX_DATA_TYPE_DOUBLE = 6; + MX_DATA_TYPE_STRING = 7; + MX_DATA_TYPE_TIME = 8; + MX_DATA_TYPE_ELAPSED_TIME = 9; + MX_DATA_TYPE_REFERENCE_TYPE = 10; + MX_DATA_TYPE_STATUS_TYPE = 11; + MX_DATA_TYPE_ENUM = 12; + MX_DATA_TYPE_SECURITY_CLASSIFICATION_ENUM = 13; + MX_DATA_TYPE_DATA_QUALITY_TYPE = 14; + MX_DATA_TYPE_QUALIFIED_ENUM = 15; + MX_DATA_TYPE_QUALIFIED_STRUCT = 16; + MX_DATA_TYPE_INTERNATIONALIZED_STRING = 17; + MX_DATA_TYPE_BIG_STRING = 18; + MX_DATA_TYPE_END = 19; +} + +message ProtocolStatus { + ProtocolStatusCode code = 1; + string message = 2; +} + +enum ProtocolStatusCode { + PROTOCOL_STATUS_CODE_UNSPECIFIED = 0; + PROTOCOL_STATUS_CODE_OK = 1; + PROTOCOL_STATUS_CODE_INVALID_REQUEST = 2; + PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND = 3; + PROTOCOL_STATUS_CODE_SESSION_NOT_READY = 4; + PROTOCOL_STATUS_CODE_WORKER_UNAVAILABLE = 5; + PROTOCOL_STATUS_CODE_TIMEOUT = 6; + PROTOCOL_STATUS_CODE_CANCELED = 7; + PROTOCOL_STATUS_CODE_PROTOCOL_VIOLATION = 8; + PROTOCOL_STATUS_CODE_MXACCESS_FAILURE = 9; +} + +enum SessionState { + SESSION_STATE_UNSPECIFIED = 0; + SESSION_STATE_CREATING = 1; + SESSION_STATE_STARTING_WORKER = 2; + SESSION_STATE_WAITING_FOR_PIPE = 3; + SESSION_STATE_HANDSHAKING = 4; + SESSION_STATE_INITIALIZING_WORKER = 5; + SESSION_STATE_READY = 6; + SESSION_STATE_CLOSING = 7; + SESSION_STATE_CLOSED = 8; + SESSION_STATE_FAULTED = 9; +} diff --git a/clients/rust/protos/mxaccess_worker.proto b/clients/rust/protos/mxaccess_worker.proto new file mode 100644 index 0000000..06a22de --- /dev/null +++ b/clients/rust/protos/mxaccess_worker.proto @@ -0,0 +1,132 @@ +syntax = "proto3"; + +package mxaccess_worker.v1; + +option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "mxaccess_gateway.proto"; + +// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves +// additively only. Never renumber or repurpose an existing field number or +// enum value. When a field or enum value is removed, add a `reserved` range +// (and `reserved` name) covering it in the same change so a future editor +// cannot accidentally reuse the retired tag. There are no `reserved` +// declarations today because no field or enum value has ever been removed. + +// Gateway-to-worker IPC envelope. Named-pipe framing prepends a little-endian +// uint32 payload length to this protobuf payload. +message WorkerEnvelope { + uint32 protocol_version = 1; + string session_id = 2; + uint64 sequence = 3; + string correlation_id = 4; + + oneof body { + GatewayHello gateway_hello = 10; + WorkerHello worker_hello = 11; + WorkerReady worker_ready = 12; + WorkerCommand worker_command = 13; + WorkerCommandReply worker_command_reply = 14; + WorkerCancel worker_cancel = 15; + WorkerShutdown worker_shutdown = 16; + WorkerShutdownAck worker_shutdown_ack = 17; + WorkerEvent worker_event = 18; + WorkerHeartbeat worker_heartbeat = 19; + WorkerFault worker_fault = 20; + } +} + +message GatewayHello { + uint32 supported_protocol_version = 1; + string nonce = 2; + string gateway_version = 3; +} + +message WorkerHello { + uint32 protocol_version = 1; + string nonce = 2; + int32 worker_process_id = 3; + string worker_version = 4; +} + +message WorkerReady { + int32 worker_process_id = 1; + string mxaccess_progid = 2; + string mxaccess_clsid = 3; + google.protobuf.Timestamp ready_timestamp = 4; +} + +message WorkerCommand { + mxaccess_gateway.v1.MxCommand command = 1; + google.protobuf.Timestamp enqueue_timestamp = 2; +} + +message WorkerCommandReply { + mxaccess_gateway.v1.MxCommandReply reply = 1; + google.protobuf.Timestamp completed_timestamp = 2; +} + +message WorkerCancel { + string reason = 1; +} + +message WorkerShutdown { + google.protobuf.Duration grace_period = 1; + string reason = 2; +} + +message WorkerShutdownAck { + mxaccess_gateway.v1.ProtocolStatus status = 1; +} + +message WorkerEvent { + mxaccess_gateway.v1.MxEvent event = 1; +} + +message WorkerHeartbeat { + int32 worker_process_id = 1; + WorkerState state = 2; + google.protobuf.Timestamp last_sta_activity_timestamp = 3; + uint32 pending_command_count = 4; + uint32 outbound_event_queue_depth = 5; + uint64 last_event_sequence = 6; + string current_command_correlation_id = 7; +} + +message WorkerFault { + WorkerFaultCategory category = 1; + string command_method = 2; + optional int32 hresult = 3; + string exception_type = 4; + string diagnostic_message = 5; + mxaccess_gateway.v1.ProtocolStatus protocol_status = 6; +} + +enum WorkerState { + WORKER_STATE_UNSPECIFIED = 0; + WORKER_STATE_STARTING = 1; + WORKER_STATE_HANDSHAKING = 2; + WORKER_STATE_INITIALIZING_STA = 3; + WORKER_STATE_READY = 4; + WORKER_STATE_EXECUTING_COMMAND = 5; + WORKER_STATE_SHUTTING_DOWN = 6; + WORKER_STATE_STOPPED = 7; + WORKER_STATE_FAULTED = 8; +} + +enum WorkerFaultCategory { + WORKER_FAULT_CATEGORY_UNSPECIFIED = 0; + WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS = 1; + WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED = 2; + WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH = 3; + WORKER_FAULT_CATEGORY_PROTOCOL_VIOLATION = 4; + WORKER_FAULT_CATEGORY_PIPE_DISCONNECTED = 5; + WORKER_FAULT_CATEGORY_MXACCESS_CREATION_FAILED = 6; + WORKER_FAULT_CATEGORY_MXACCESS_COMMAND_FAILED = 7; + WORKER_FAULT_CATEGORY_MXACCESS_EVENT_CONVERSION_FAILED = 8; + WORKER_FAULT_CATEGORY_STA_HUNG = 9; + WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW = 10; + WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT = 11; +} diff --git a/scripts/pack-clients.ps1 b/scripts/pack-clients.ps1 index 12aa220..a3b7b21 100644 --- a/scripts/pack-clients.ps1 +++ b/scripts/pack-clients.ps1 @@ -187,7 +187,9 @@ function Invoke-PackRust { } Write-Host 'Running cargo package...' - & cargo package --no-verify + # No --no-verify: the crate vendors its protos (clients/rust/protos) so + # the verify build proves it compiles standalone from the tarball (CLI-02). + & cargo package if ($LASTEXITCODE -ne 0) { throw 'cargo package failed.' } $packageDir = Join-Path $rustDir 'target/package' @@ -207,7 +209,7 @@ function Invoke-PackRust { Write-Host 'Publishing Rust crate to Gitea...' -ForegroundColor Yellow Push-Location (Join-Path $RepoRoot 'clients/rust') try { - & cargo publish --no-verify --registry dohertj2-gitea + & cargo publish --registry dohertj2-gitea if ($LASTEXITCODE -ne 0) { throw 'cargo publish failed.' } } finally { Pop-Location } }