Compare commits

..

6 Commits

Author SHA1 Message Date
Joseph Doherty 864b9f4bd3 Remove the AlarmClientDiscovery probe log
Delete docs/AlarmClientDiscovery.md — an archival AVEVA alarm-consumer
investigation log whose durable findings now live in the alarm
worker/monitor code. Drop the now-dangling links from Grpc.md and
GatewayConfiguration.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:04:29 -04:00
Joseph Doherty de58872435 Document the session-less StreamAlarms feed and alarm config
Update the gateway docs for the central alarm monitor reversal:
Grpc.md replaces QueryActiveAlarms with the session-less StreamAlarms
RPC and notes AcknowledgeAlarm no longer needs a session;
Authorization.md maps StreamAlarmsRequest to events:read;
GatewayConfiguration.md adds the MxGateway:Alarms options block; and
GatewayDashboardDesign.md points the Alarms page at the central
monitor cache instead of a per-session subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:59:48 -04:00
Joseph Doherty 6777d49030 Point the Java client at the StreamAlarms alarm feed
Regenerate the Java protobuf stubs and replace queryActiveAlarms with
streamAlarms, returning a MxGatewayAlarmFeedSubscription over
AlarmFeedMessage served by the gateway's central alarm monitor
(snapshot, snapshot_complete, then live transitions). Drops session_id
from the acknowledge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:55:20 -04:00
Joseph Doherty 1b6ca07bb5 Point the Rust client at the StreamAlarms alarm feed
Replace GatewayClient::query_active_alarms with stream_alarms, an
AlarmFeedStream over AlarmFeedMessage served by the gateway's central
alarm monitor (snapshot, snapshot_complete, then live transitions).
Drops session_id from the acknowledge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:49:26 -04:00
Joseph Doherty 1ad0be8276 Point the Python client at the StreamAlarms alarm feed
Regenerate the Python protobuf stubs and replace query_active_alarms
with stream_alarms, an AsyncIterator over AlarmFeedMessage served by
the gateway's central alarm monitor (snapshot, snapshot_complete, then
live transitions). Drops session_id from the acknowledge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:45:53 -04:00
Joseph Doherty 9328c4f657 Point the Go client at the StreamAlarms alarm feed
Regenerate the Go protobuf stubs and replace the session-scoped
QueryActiveAlarms surface with the session-less StreamAlarms feed:
snapshot-then-live AlarmFeedMessage fan-out served by the gateway's
central alarm monitor. Drops session_id from the acknowledge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:45:47 -04:00
23 changed files with 2360 additions and 2216 deletions
@@ -687,18 +687,32 @@ func (x *GalaxyObject) GetAttributes() []*GalaxyAttribute {
}
type GalaxyAttribute struct {
state protoimpl.MessageState `protogen:"open.v1"`
AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3" json:"attribute_name,omitempty"`
FullTagReference string `protobuf:"bytes,2,opt,name=full_tag_reference,json=fullTagReference,proto3" json:"full_tag_reference,omitempty"`
MxDataType int32 `protobuf:"varint,3,opt,name=mx_data_type,json=mxDataType,proto3" json:"mx_data_type,omitempty"`
DataTypeName string `protobuf:"bytes,4,opt,name=data_type_name,json=dataTypeName,proto3" json:"data_type_name,omitempty"`
IsArray bool `protobuf:"varint,5,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"`
ArrayDimension int32 `protobuf:"varint,6,opt,name=array_dimension,json=arrayDimension,proto3" json:"array_dimension,omitempty"`
ArrayDimensionPresent bool `protobuf:"varint,7,opt,name=array_dimension_present,json=arrayDimensionPresent,proto3" json:"array_dimension_present,omitempty"`
MxAttributeCategory int32 `protobuf:"varint,8,opt,name=mx_attribute_category,json=mxAttributeCategory,proto3" json:"mx_attribute_category,omitempty"`
SecurityClassification int32 `protobuf:"varint,9,opt,name=security_classification,json=securityClassification,proto3" json:"security_classification,omitempty"`
IsHistorized bool `protobuf:"varint,10,opt,name=is_historized,json=isHistorized,proto3" json:"is_historized,omitempty"`
IsAlarm bool `protobuf:"varint,11,opt,name=is_alarm,json=isAlarm,proto3" json:"is_alarm,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3" json:"attribute_name,omitempty"`
FullTagReference string `protobuf:"bytes,2,opt,name=full_tag_reference,json=fullTagReference,proto3" json:"full_tag_reference,omitempty"`
// 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.
MxDataType int32 `protobuf:"varint,3,opt,name=mx_data_type,json=mxDataType,proto3" json:"mx_data_type,omitempty"`
// Human-readable name from Galaxy's `dbo.data_type` table (e.g. "Float",
// "Integer", "Boolean"). Free-form Galaxy text; not a stable enum.
DataTypeName string `protobuf:"bytes,4,opt,name=data_type_name,json=dataTypeName,proto3" json:"data_type_name,omitempty"`
IsArray bool `protobuf:"varint,5,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"`
ArrayDimension int32 `protobuf:"varint,6,opt,name=array_dimension,json=arrayDimension,proto3" json:"array_dimension,omitempty"`
ArrayDimensionPresent bool `protobuf:"varint,7,opt,name=array_dimension_present,json=arrayDimensionPresent,proto3" json:"array_dimension_present,omitempty"`
// Raw Galaxy SQL attribute-category identifier, passed through unchanged.
// Galaxy-specific; not mapped to any gateway enum. See
// docs/GalaxyRepository.md.
MxAttributeCategory int32 `protobuf:"varint,8,opt,name=mx_attribute_category,json=mxAttributeCategory,proto3" json:"mx_attribute_category,omitempty"`
// Raw Galaxy SQL security-classification identifier, passed through
// unchanged. Galaxy-specific; not mapped to any gateway enum. See
// docs/GalaxyRepository.md.
SecurityClassification int32 `protobuf:"varint,9,opt,name=security_classification,json=securityClassification,proto3" json:"security_classification,omitempty"`
IsHistorized bool `protobuf:"varint,10,opt,name=is_historized,json=isHistorized,proto3" json:"is_historized,omitempty"`
IsAlarm bool `protobuf:"varint,11,opt,name=is_alarm,json=isAlarm,proto3" json:"is_alarm,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -3792,9 +3792,11 @@ type WriteSecuredBulkEntry struct {
ItemHandle int32 `protobuf:"varint,1,opt,name=item_handle,json=itemHandle,proto3" json:"item_handle,omitempty"`
CurrentUserId int32 `protobuf:"varint,2,opt,name=current_user_id,json=currentUserId,proto3" json:"current_user_id,omitempty"`
VerifierUserId int32 `protobuf:"varint,3,opt,name=verifier_user_id,json=verifierUserId,proto3" json:"verifier_user_id,omitempty"`
Value *MxValue `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// Credential-sensitive write value. Implementations must not log this field
// unless an explicit redacted value-logging path is enabled.
Value *MxValue `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WriteSecuredBulkEntry) Reset() {
@@ -3914,8 +3916,10 @@ type WriteSecured2BulkEntry struct {
ItemHandle int32 `protobuf:"varint,1,opt,name=item_handle,json=itemHandle,proto3" json:"item_handle,omitempty"`
CurrentUserId int32 `protobuf:"varint,2,opt,name=current_user_id,json=currentUserId,proto3" json:"current_user_id,omitempty"`
VerifierUserId int32 `protobuf:"varint,3,opt,name=verifier_user_id,json=verifierUserId,proto3" json:"verifier_user_id,omitempty"`
Value *MxValue `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
TimestampValue *MxValue `protobuf:"bytes,5,opt,name=timestamp_value,json=timestampValue,proto3" json:"timestamp_value,omitempty"`
// Credential-sensitive write value. Implementations must not log this field
// unless an explicit redacted value-logging path is enabled.
Value *MxValue `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
TimestampValue *MxValue `protobuf:"bytes,5,opt,name=timestamp_value,json=timestampValue,proto3" json:"timestamp_value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -3987,6 +3991,7 @@ func (x *WriteSecured2BulkEntry) GetTimestampValue() *MxValue {
// 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
@@ -5245,9 +5250,11 @@ func (x *BulkSubscribeReply) GetResults() []*SubscribeResult {
// 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 tag-allowlist filter dropped some entries before reaching
// the worker. Per-item failures populate `error_message` + `hresult` and never
// raise — callers iterate and inspect each entry.
// 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.
type BulkWriteResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServerHandle int32 `protobuf:"varint,1,opt,name=server_handle,json=serverHandle,proto3" json:"server_handle,omitempty"`
@@ -5380,6 +5387,20 @@ func (x *BulkWriteReply) GetResults() []*BulkWriteResult {
// 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.
type BulkReadResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServerHandle int32 `protobuf:"varint,1,opt,name=server_handle,json=serverHandle,proto3" json:"server_handle,omitempty"`
@@ -6528,7 +6549,6 @@ func (x *ActiveAlarmSnapshot) GetLimitValue() *MxValue {
type AcknowledgeAlarmRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
ClientCorrelationId string `protobuf:"bytes,2,opt,name=client_correlation_id,json=clientCorrelationId,proto3" json:"client_correlation_id,omitempty"`
// Fully-qualified alarm reference matching OnAlarmTransitionEvent.alarm_full_reference.
AlarmFullReference string `protobuf:"bytes,3,opt,name=alarm_full_reference,json=alarmFullReference,proto3" json:"alarm_full_reference,omitempty"`
@@ -6571,13 +6591,6 @@ func (*AcknowledgeAlarmRequest) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{77}
}
func (x *AcknowledgeAlarmRequest) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *AcknowledgeAlarmRequest) GetClientCorrelationId() string {
if x != nil {
return x.ClientCorrelationId
@@ -6608,7 +6621,6 @@ func (x *AcknowledgeAlarmRequest) GetOperatorUser() string {
type AcknowledgeAlarmReply struct {
state protoimpl.MessageState `protogen:"open.v1"`
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
CorrelationId string `protobuf:"bytes,2,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty"`
ProtocolStatus *ProtocolStatus `protobuf:"bytes,3,opt,name=protocol_status,json=protocolStatus,proto3" json:"protocol_status,omitempty"`
// Native ack return code echoed from the worker. The worker carries the
@@ -6659,13 +6671,6 @@ func (*AcknowledgeAlarmReply) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{78}
}
func (x *AcknowledgeAlarmReply) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *AcknowledgeAlarmReply) GetCorrelationId() string {
if x != nil {
return x.CorrelationId
@@ -6701,31 +6706,31 @@ func (x *AcknowledgeAlarmReply) GetDiagnosticMessage() string {
return ""
}
type QueryActiveAlarmsRequest struct {
// Request to attach to the gateway's central alarm feed (StreamAlarms).
type StreamAlarmsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
ClientCorrelationId string `protobuf:"bytes,2,opt,name=client_correlation_id,json=clientCorrelationId,proto3" json:"client_correlation_id,omitempty"`
// Optional alarm-reference prefix used to scope a partial ConditionRefresh
// (e.g. equipment sub-tree). Empty means full refresh.
AlarmFilterPrefix string `protobuf:"bytes,3,opt,name=alarm_filter_prefix,json=alarmFilterPrefix,proto3" json:"alarm_filter_prefix,omitempty"`
ClientCorrelationId string `protobuf:"bytes,1,opt,name=client_correlation_id,json=clientCorrelationId,proto3" json:"client_correlation_id,omitempty"`
// Optional alarm-reference prefix scoping the feed to an equipment
// sub-tree. Empty streams every active alarm.
AlarmFilterPrefix string `protobuf:"bytes,2,opt,name=alarm_filter_prefix,json=alarmFilterPrefix,proto3" json:"alarm_filter_prefix,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *QueryActiveAlarmsRequest) Reset() {
*x = QueryActiveAlarmsRequest{}
func (x *StreamAlarmsRequest) Reset() {
*x = StreamAlarmsRequest{}
mi := &file_mxaccess_gateway_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *QueryActiveAlarmsRequest) String() string {
func (x *StreamAlarmsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QueryActiveAlarmsRequest) ProtoMessage() {}
func (*StreamAlarmsRequest) ProtoMessage() {}
func (x *QueryActiveAlarmsRequest) ProtoReflect() protoreflect.Message {
func (x *StreamAlarmsRequest) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -6737,32 +6742,130 @@ func (x *QueryActiveAlarmsRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use QueryActiveAlarmsRequest.ProtoReflect.Descriptor instead.
func (*QueryActiveAlarmsRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use StreamAlarmsRequest.ProtoReflect.Descriptor instead.
func (*StreamAlarmsRequest) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{79}
}
func (x *QueryActiveAlarmsRequest) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *QueryActiveAlarmsRequest) GetClientCorrelationId() string {
func (x *StreamAlarmsRequest) GetClientCorrelationId() string {
if x != nil {
return x.ClientCorrelationId
}
return ""
}
func (x *QueryActiveAlarmsRequest) GetAlarmFilterPrefix() string {
func (x *StreamAlarmsRequest) GetAlarmFilterPrefix() string {
if x != nil {
return x.AlarmFilterPrefix
}
return ""
}
// 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.
type AlarmFeedMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
//
// *AlarmFeedMessage_ActiveAlarm
// *AlarmFeedMessage_SnapshotComplete
// *AlarmFeedMessage_Transition
Payload isAlarmFeedMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AlarmFeedMessage) Reset() {
*x = AlarmFeedMessage{}
mi := &file_mxaccess_gateway_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AlarmFeedMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AlarmFeedMessage) ProtoMessage() {}
func (x *AlarmFeedMessage) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AlarmFeedMessage.ProtoReflect.Descriptor instead.
func (*AlarmFeedMessage) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{80}
}
func (x *AlarmFeedMessage) GetPayload() isAlarmFeedMessage_Payload {
if x != nil {
return x.Payload
}
return nil
}
func (x *AlarmFeedMessage) GetActiveAlarm() *ActiveAlarmSnapshot {
if x != nil {
if x, ok := x.Payload.(*AlarmFeedMessage_ActiveAlarm); ok {
return x.ActiveAlarm
}
}
return nil
}
func (x *AlarmFeedMessage) GetSnapshotComplete() bool {
if x != nil {
if x, ok := x.Payload.(*AlarmFeedMessage_SnapshotComplete); ok {
return x.SnapshotComplete
}
}
return false
}
func (x *AlarmFeedMessage) GetTransition() *OnAlarmTransitionEvent {
if x != nil {
if x, ok := x.Payload.(*AlarmFeedMessage_Transition); ok {
return x.Transition
}
}
return nil
}
type isAlarmFeedMessage_Payload interface {
isAlarmFeedMessage_Payload()
}
type AlarmFeedMessage_ActiveAlarm struct {
// Part of the initial active-alarm snapshot (ConditionRefresh).
ActiveAlarm *ActiveAlarmSnapshot `protobuf:"bytes,1,opt,name=active_alarm,json=activeAlarm,proto3,oneof"`
}
type AlarmFeedMessage_SnapshotComplete struct {
// Sentinel: the initial snapshot is fully delivered and `transition`
// messages follow. Always true when present.
SnapshotComplete bool `protobuf:"varint,2,opt,name=snapshot_complete,json=snapshotComplete,proto3,oneof"`
}
type AlarmFeedMessage_Transition struct {
// A live alarm state change (raise / acknowledge / clear).
Transition *OnAlarmTransitionEvent `protobuf:"bytes,3,opt,name=transition,proto3,oneof"`
}
func (*AlarmFeedMessage_ActiveAlarm) isAlarmFeedMessage_Payload() {}
func (*AlarmFeedMessage_SnapshotComplete) isAlarmFeedMessage_Payload() {}
func (*AlarmFeedMessage_Transition) isAlarmFeedMessage_Payload() {}
type MxStatusProxy struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
@@ -6787,7 +6890,7 @@ type MxStatusProxy struct {
func (x *MxStatusProxy) Reset() {
*x = MxStatusProxy{}
mi := &file_mxaccess_gateway_proto_msgTypes[80]
mi := &file_mxaccess_gateway_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6799,7 +6902,7 @@ func (x *MxStatusProxy) String() string {
func (*MxStatusProxy) ProtoMessage() {}
func (x *MxStatusProxy) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[80]
mi := &file_mxaccess_gateway_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6812,7 +6915,7 @@ func (x *MxStatusProxy) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxStatusProxy.ProtoReflect.Descriptor instead.
func (*MxStatusProxy) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{80}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{81}
}
func (x *MxStatusProxy) GetSuccess() int32 {
@@ -6889,7 +6992,7 @@ type MxValue struct {
func (x *MxValue) Reset() {
*x = MxValue{}
mi := &file_mxaccess_gateway_proto_msgTypes[81]
mi := &file_mxaccess_gateway_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6901,7 +7004,7 @@ func (x *MxValue) String() string {
func (*MxValue) ProtoMessage() {}
func (x *MxValue) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[81]
mi := &file_mxaccess_gateway_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6914,7 +7017,7 @@ func (x *MxValue) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxValue.ProtoReflect.Descriptor instead.
func (*MxValue) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{81}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{82}
}
func (x *MxValue) GetDataType() MxDataType {
@@ -7122,7 +7225,7 @@ type MxArray struct {
func (x *MxArray) Reset() {
*x = MxArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[82]
mi := &file_mxaccess_gateway_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7134,7 +7237,7 @@ func (x *MxArray) String() string {
func (*MxArray) ProtoMessage() {}
func (x *MxArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[82]
mi := &file_mxaccess_gateway_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7147,7 +7250,7 @@ func (x *MxArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxArray.ProtoReflect.Descriptor instead.
func (*MxArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{82}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{83}
}
func (x *MxArray) GetElementDataType() MxDataType {
@@ -7325,7 +7428,7 @@ type BoolArray struct {
func (x *BoolArray) Reset() {
*x = BoolArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[83]
mi := &file_mxaccess_gateway_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7337,7 +7440,7 @@ func (x *BoolArray) String() string {
func (*BoolArray) ProtoMessage() {}
func (x *BoolArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[83]
mi := &file_mxaccess_gateway_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7350,7 +7453,7 @@ func (x *BoolArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use BoolArray.ProtoReflect.Descriptor instead.
func (*BoolArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{83}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{84}
}
func (x *BoolArray) GetValues() []bool {
@@ -7369,7 +7472,7 @@ type Int32Array struct {
func (x *Int32Array) Reset() {
*x = Int32Array{}
mi := &file_mxaccess_gateway_proto_msgTypes[84]
mi := &file_mxaccess_gateway_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7381,7 +7484,7 @@ func (x *Int32Array) String() string {
func (*Int32Array) ProtoMessage() {}
func (x *Int32Array) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[84]
mi := &file_mxaccess_gateway_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7394,7 +7497,7 @@ func (x *Int32Array) ProtoReflect() protoreflect.Message {
// Deprecated: Use Int32Array.ProtoReflect.Descriptor instead.
func (*Int32Array) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{84}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{85}
}
func (x *Int32Array) GetValues() []int32 {
@@ -7413,7 +7516,7 @@ type Int64Array struct {
func (x *Int64Array) Reset() {
*x = Int64Array{}
mi := &file_mxaccess_gateway_proto_msgTypes[85]
mi := &file_mxaccess_gateway_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7425,7 +7528,7 @@ func (x *Int64Array) String() string {
func (*Int64Array) ProtoMessage() {}
func (x *Int64Array) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[85]
mi := &file_mxaccess_gateway_proto_msgTypes[86]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7438,7 +7541,7 @@ func (x *Int64Array) ProtoReflect() protoreflect.Message {
// Deprecated: Use Int64Array.ProtoReflect.Descriptor instead.
func (*Int64Array) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{85}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{86}
}
func (x *Int64Array) GetValues() []int64 {
@@ -7457,7 +7560,7 @@ type FloatArray struct {
func (x *FloatArray) Reset() {
*x = FloatArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[86]
mi := &file_mxaccess_gateway_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7469,7 +7572,7 @@ func (x *FloatArray) String() string {
func (*FloatArray) ProtoMessage() {}
func (x *FloatArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[86]
mi := &file_mxaccess_gateway_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7482,7 +7585,7 @@ func (x *FloatArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use FloatArray.ProtoReflect.Descriptor instead.
func (*FloatArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{86}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{87}
}
func (x *FloatArray) GetValues() []float32 {
@@ -7501,7 +7604,7 @@ type DoubleArray struct {
func (x *DoubleArray) Reset() {
*x = DoubleArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[87]
mi := &file_mxaccess_gateway_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7513,7 +7616,7 @@ func (x *DoubleArray) String() string {
func (*DoubleArray) ProtoMessage() {}
func (x *DoubleArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[87]
mi := &file_mxaccess_gateway_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7526,7 +7629,7 @@ func (x *DoubleArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use DoubleArray.ProtoReflect.Descriptor instead.
func (*DoubleArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{87}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{88}
}
func (x *DoubleArray) GetValues() []float64 {
@@ -7545,7 +7648,7 @@ type StringArray struct {
func (x *StringArray) Reset() {
*x = StringArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[88]
mi := &file_mxaccess_gateway_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7557,7 +7660,7 @@ func (x *StringArray) String() string {
func (*StringArray) ProtoMessage() {}
func (x *StringArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[88]
mi := &file_mxaccess_gateway_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7570,7 +7673,7 @@ func (x *StringArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use StringArray.ProtoReflect.Descriptor instead.
func (*StringArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{88}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{89}
}
func (x *StringArray) GetValues() []string {
@@ -7589,7 +7692,7 @@ type TimestampArray struct {
func (x *TimestampArray) Reset() {
*x = TimestampArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[89]
mi := &file_mxaccess_gateway_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7601,7 +7704,7 @@ func (x *TimestampArray) String() string {
func (*TimestampArray) ProtoMessage() {}
func (x *TimestampArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[89]
mi := &file_mxaccess_gateway_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7614,7 +7717,7 @@ func (x *TimestampArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use TimestampArray.ProtoReflect.Descriptor instead.
func (*TimestampArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{89}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{90}
}
func (x *TimestampArray) GetValues() []*timestamppb.Timestamp {
@@ -7633,7 +7736,7 @@ type RawArray struct {
func (x *RawArray) Reset() {
*x = RawArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[90]
mi := &file_mxaccess_gateway_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7645,7 +7748,7 @@ func (x *RawArray) String() string {
func (*RawArray) ProtoMessage() {}
func (x *RawArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[90]
mi := &file_mxaccess_gateway_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7658,7 +7761,7 @@ func (x *RawArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use RawArray.ProtoReflect.Descriptor instead.
func (*RawArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{90}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{91}
}
func (x *RawArray) GetValues() [][]byte {
@@ -7678,7 +7781,7 @@ type ProtocolStatus struct {
func (x *ProtocolStatus) Reset() {
*x = ProtocolStatus{}
mi := &file_mxaccess_gateway_proto_msgTypes[91]
mi := &file_mxaccess_gateway_proto_msgTypes[92]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7690,7 +7793,7 @@ func (x *ProtocolStatus) String() string {
func (*ProtocolStatus) ProtoMessage() {}
func (x *ProtocolStatus) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[91]
mi := &file_mxaccess_gateway_proto_msgTypes[92]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7703,7 +7806,7 @@ func (x *ProtocolStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProtocolStatus.ProtoReflect.Descriptor instead.
func (*ProtocolStatus) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{91}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{92}
}
func (x *ProtocolStatus) GetCode() ProtocolStatusCode {
@@ -8155,29 +8258,32 @@ const file_mxaccess_gateway_proto_rawDesc = "" +
"\x10operator_comment\x18\v \x01(\tR\x0foperatorComment\x12A\n" +
"\rcurrent_value\x18\f \x01(\v2\x1c.mxaccess_gateway.v1.MxValueR\fcurrentValue\x12=\n" +
"\vlimit_value\x18\r \x01(\v2\x1c.mxaccess_gateway.v1.MxValueR\n" +
"limitValue\"\xdd\x01\n" +
"\x17AcknowledgeAlarmRequest\x12\x1d\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x122\n" +
"limitValue\"\xd0\x01\n" +
"\x17AcknowledgeAlarmRequest\x122\n" +
"\x15client_correlation_id\x18\x02 \x01(\tR\x13clientCorrelationId\x120\n" +
"\x14alarm_full_reference\x18\x03 \x01(\tR\x12alarmFullReference\x12\x18\n" +
"\acomment\x18\x04 \x01(\tR\acomment\x12#\n" +
"\roperator_user\x18\x05 \x01(\tR\foperatorUser\"\xc1\x02\n" +
"\x15AcknowledgeAlarmReply\x12\x1d\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x12%\n" +
"\roperator_user\x18\x05 \x01(\tR\foperatorUserJ\x04\b\x01\x10\x02R\n" +
"session_id\"\xb4\x02\n" +
"\x15AcknowledgeAlarmReply\x12%\n" +
"\x0ecorrelation_id\x18\x02 \x01(\tR\rcorrelationId\x12L\n" +
"\x0fprotocol_status\x18\x03 \x01(\v2#.mxaccess_gateway.v1.ProtocolStatusR\x0eprotocolStatus\x12\x1d\n" +
"\ahresult\x18\x04 \x01(\x05H\x00R\ahresult\x88\x01\x01\x12:\n" +
"\x06status\x18\x05 \x01(\v2\".mxaccess_gateway.v1.MxStatusProxyR\x06status\x12-\n" +
"\x12diagnostic_message\x18\x06 \x01(\tR\x11diagnosticMessageB\n" +
"\n" +
"\b_hresult\"\x9d\x01\n" +
"\x18QueryActiveAlarmsRequest\x12\x1d\n" +
"\b_hresultJ\x04\b\x01\x10\x02R\n" +
"session_id\"y\n" +
"\x13StreamAlarmsRequest\x122\n" +
"\x15client_correlation_id\x18\x01 \x01(\tR\x13clientCorrelationId\x12.\n" +
"\x13alarm_filter_prefix\x18\x02 \x01(\tR\x11alarmFilterPrefix\"\xea\x01\n" +
"\x10AlarmFeedMessage\x12M\n" +
"\factive_alarm\x18\x01 \x01(\v2(.mxaccess_gateway.v1.ActiveAlarmSnapshotH\x00R\vactiveAlarm\x12-\n" +
"\x11snapshot_complete\x18\x02 \x01(\bH\x00R\x10snapshotComplete\x12M\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x122\n" +
"\x15client_correlation_id\x18\x02 \x01(\tR\x13clientCorrelationId\x12.\n" +
"\x13alarm_filter_prefix\x18\x03 \x01(\tR\x11alarmFilterPrefix\"\xbe\x02\n" +
"transition\x18\x03 \x01(\v2+.mxaccess_gateway.v1.OnAlarmTransitionEventH\x00R\n" +
"transitionB\t\n" +
"\apayload\"\xbe\x02\n" +
"\rMxStatusProxy\x12\x18\n" +
"\asuccess\x18\x01 \x01(\x05R\asuccess\x12A\n" +
"\bcategory\x18\x02 \x01(\x0e2%.mxaccess_gateway.v1.MxStatusCategoryR\bcategory\x12D\n" +
@@ -8377,14 +8483,14 @@ const file_mxaccess_gateway_proto_rawDesc = "" +
"\x13SESSION_STATE_READY\x10\x06\x12\x19\n" +
"\x15SESSION_STATE_CLOSING\x10\a\x12\x18\n" +
"\x14SESSION_STATE_CLOSED\x10\b\x12\x19\n" +
"\x15SESSION_STATE_FAULTED\x10\t2\xe0\x04\n" +
"\x15SESSION_STATE_FAULTED\x10\t2\xd3\x04\n" +
"\x0fMxAccessGateway\x12]\n" +
"\vOpenSession\x12'.mxaccess_gateway.v1.OpenSessionRequest\x1a%.mxaccess_gateway.v1.OpenSessionReply\x12`\n" +
"\fCloseSession\x12(.mxaccess_gateway.v1.CloseSessionRequest\x1a&.mxaccess_gateway.v1.CloseSessionReply\x12T\n" +
"\x06Invoke\x12%.mxaccess_gateway.v1.MxCommandRequest\x1a#.mxaccess_gateway.v1.MxCommandReply\x12X\n" +
"\fStreamEvents\x12(.mxaccess_gateway.v1.StreamEventsRequest\x1a\x1c.mxaccess_gateway.v1.MxEvent0\x01\x12l\n" +
"\x10AcknowledgeAlarm\x12,.mxaccess_gateway.v1.AcknowledgeAlarmRequest\x1a*.mxaccess_gateway.v1.AcknowledgeAlarmReply\x12n\n" +
"\x11QueryActiveAlarms\x12-.mxaccess_gateway.v1.QueryActiveAlarmsRequest\x1a(.mxaccess_gateway.v1.ActiveAlarmSnapshot0\x01B\x1c\xaa\x02\x19MxGateway.Contracts.Protob\x06proto3"
"\x10AcknowledgeAlarm\x12,.mxaccess_gateway.v1.AcknowledgeAlarmRequest\x1a*.mxaccess_gateway.v1.AcknowledgeAlarmReply\x12a\n" +
"\fStreamAlarms\x12(.mxaccess_gateway.v1.StreamAlarmsRequest\x1a%.mxaccess_gateway.v1.AlarmFeedMessage0\x01B\x1c\xaa\x02\x19MxGateway.Contracts.Protob\x06proto3"
var (
file_mxaccess_gateway_proto_rawDescOnce sync.Once
@@ -8399,7 +8505,7 @@ func file_mxaccess_gateway_proto_rawDescGZIP() []byte {
}
var file_mxaccess_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 9)
var file_mxaccess_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 92)
var file_mxaccess_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 93)
var file_mxaccess_gateway_proto_goTypes = []any{
(MxCommandKind)(0), // 0: mxaccess_gateway.v1.MxCommandKind
(MxEventFamily)(0), // 1: mxaccess_gateway.v1.MxEventFamily
@@ -8489,28 +8595,29 @@ var file_mxaccess_gateway_proto_goTypes = []any{
(*ActiveAlarmSnapshot)(nil), // 85: mxaccess_gateway.v1.ActiveAlarmSnapshot
(*AcknowledgeAlarmRequest)(nil), // 86: mxaccess_gateway.v1.AcknowledgeAlarmRequest
(*AcknowledgeAlarmReply)(nil), // 87: mxaccess_gateway.v1.AcknowledgeAlarmReply
(*QueryActiveAlarmsRequest)(nil), // 88: mxaccess_gateway.v1.QueryActiveAlarmsRequest
(*MxStatusProxy)(nil), // 89: mxaccess_gateway.v1.MxStatusProxy
(*MxValue)(nil), // 90: mxaccess_gateway.v1.MxValue
(*MxArray)(nil), // 91: mxaccess_gateway.v1.MxArray
(*BoolArray)(nil), // 92: mxaccess_gateway.v1.BoolArray
(*Int32Array)(nil), // 93: mxaccess_gateway.v1.Int32Array
(*Int64Array)(nil), // 94: mxaccess_gateway.v1.Int64Array
(*FloatArray)(nil), // 95: mxaccess_gateway.v1.FloatArray
(*DoubleArray)(nil), // 96: mxaccess_gateway.v1.DoubleArray
(*StringArray)(nil), // 97: mxaccess_gateway.v1.StringArray
(*TimestampArray)(nil), // 98: mxaccess_gateway.v1.TimestampArray
(*RawArray)(nil), // 99: mxaccess_gateway.v1.RawArray
(*ProtocolStatus)(nil), // 100: mxaccess_gateway.v1.ProtocolStatus
(*durationpb.Duration)(nil), // 101: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 102: google.protobuf.Timestamp
(*StreamAlarmsRequest)(nil), // 88: mxaccess_gateway.v1.StreamAlarmsRequest
(*AlarmFeedMessage)(nil), // 89: mxaccess_gateway.v1.AlarmFeedMessage
(*MxStatusProxy)(nil), // 90: mxaccess_gateway.v1.MxStatusProxy
(*MxValue)(nil), // 91: mxaccess_gateway.v1.MxValue
(*MxArray)(nil), // 92: mxaccess_gateway.v1.MxArray
(*BoolArray)(nil), // 93: mxaccess_gateway.v1.BoolArray
(*Int32Array)(nil), // 94: mxaccess_gateway.v1.Int32Array
(*Int64Array)(nil), // 95: mxaccess_gateway.v1.Int64Array
(*FloatArray)(nil), // 96: mxaccess_gateway.v1.FloatArray
(*DoubleArray)(nil), // 97: mxaccess_gateway.v1.DoubleArray
(*StringArray)(nil), // 98: mxaccess_gateway.v1.StringArray
(*TimestampArray)(nil), // 99: mxaccess_gateway.v1.TimestampArray
(*RawArray)(nil), // 100: mxaccess_gateway.v1.RawArray
(*ProtocolStatus)(nil), // 101: mxaccess_gateway.v1.ProtocolStatus
(*durationpb.Duration)(nil), // 102: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 103: google.protobuf.Timestamp
}
var file_mxaccess_gateway_proto_depIdxs = []int32{
101, // 0: mxaccess_gateway.v1.OpenSessionRequest.command_timeout:type_name -> google.protobuf.Duration
101, // 1: mxaccess_gateway.v1.OpenSessionReply.default_command_timeout:type_name -> google.protobuf.Duration
100, // 2: mxaccess_gateway.v1.OpenSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
102, // 0: mxaccess_gateway.v1.OpenSessionRequest.command_timeout:type_name -> google.protobuf.Duration
102, // 1: mxaccess_gateway.v1.OpenSessionReply.default_command_timeout:type_name -> google.protobuf.Duration
101, // 2: mxaccess_gateway.v1.OpenSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
8, // 3: mxaccess_gateway.v1.CloseSessionReply.final_state:type_name -> mxaccess_gateway.v1.SessionState
100, // 4: mxaccess_gateway.v1.CloseSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
101, // 4: mxaccess_gateway.v1.CloseSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
15, // 5: mxaccess_gateway.v1.MxCommandRequest.command:type_name -> mxaccess_gateway.v1.MxCommand
0, // 6: mxaccess_gateway.v1.MxCommand.kind:type_name -> mxaccess_gateway.v1.MxCommandKind
16, // 7: mxaccess_gateway.v1.MxCommand.register:type_name -> mxaccess_gateway.v1.RegisterCommand
@@ -8552,27 +8659,27 @@ var file_mxaccess_gateway_proto_depIdxs = []int32{
56, // 43: mxaccess_gateway.v1.MxCommand.get_worker_info:type_name -> mxaccess_gateway.v1.GetWorkerInfoCommand
57, // 44: mxaccess_gateway.v1.MxCommand.drain_events:type_name -> mxaccess_gateway.v1.DrainEventsCommand
58, // 45: mxaccess_gateway.v1.MxCommand.shutdown_worker:type_name -> mxaccess_gateway.v1.ShutdownWorkerCommand
90, // 46: mxaccess_gateway.v1.WriteCommand.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 47: mxaccess_gateway.v1.Write2Command.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 48: mxaccess_gateway.v1.Write2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
90, // 49: mxaccess_gateway.v1.WriteSecuredCommand.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 50: mxaccess_gateway.v1.WriteSecured2Command.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 51: mxaccess_gateway.v1.WriteSecured2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
91, // 46: mxaccess_gateway.v1.WriteCommand.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 47: mxaccess_gateway.v1.Write2Command.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 48: mxaccess_gateway.v1.Write2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
91, // 49: mxaccess_gateway.v1.WriteSecuredCommand.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 50: mxaccess_gateway.v1.WriteSecured2Command.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 51: mxaccess_gateway.v1.WriteSecured2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
46, // 52: mxaccess_gateway.v1.WriteBulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteBulkEntry
90, // 53: mxaccess_gateway.v1.WriteBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 53: mxaccess_gateway.v1.WriteBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
48, // 54: mxaccess_gateway.v1.Write2BulkCommand.entries:type_name -> mxaccess_gateway.v1.Write2BulkEntry
90, // 55: mxaccess_gateway.v1.Write2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 56: mxaccess_gateway.v1.Write2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
91, // 55: mxaccess_gateway.v1.Write2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 56: mxaccess_gateway.v1.Write2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
50, // 57: mxaccess_gateway.v1.WriteSecuredBulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteSecuredBulkEntry
90, // 58: mxaccess_gateway.v1.WriteSecuredBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 58: mxaccess_gateway.v1.WriteSecuredBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
52, // 59: mxaccess_gateway.v1.WriteSecured2BulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteSecured2BulkEntry
90, // 60: mxaccess_gateway.v1.WriteSecured2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
90, // 61: mxaccess_gateway.v1.WriteSecured2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
101, // 62: mxaccess_gateway.v1.ShutdownWorkerCommand.grace_period:type_name -> google.protobuf.Duration
91, // 60: mxaccess_gateway.v1.WriteSecured2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
91, // 61: mxaccess_gateway.v1.WriteSecured2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
102, // 62: mxaccess_gateway.v1.ShutdownWorkerCommand.grace_period:type_name -> google.protobuf.Duration
0, // 63: mxaccess_gateway.v1.MxCommandReply.kind:type_name -> mxaccess_gateway.v1.MxCommandKind
100, // 64: mxaccess_gateway.v1.MxCommandReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
90, // 65: mxaccess_gateway.v1.MxCommandReply.return_value:type_name -> mxaccess_gateway.v1.MxValue
89, // 66: mxaccess_gateway.v1.MxCommandReply.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
101, // 64: mxaccess_gateway.v1.MxCommandReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
91, // 65: mxaccess_gateway.v1.MxCommandReply.return_value:type_name -> mxaccess_gateway.v1.MxValue
90, // 66: mxaccess_gateway.v1.MxCommandReply.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
60, // 67: mxaccess_gateway.v1.MxCommandReply.register:type_name -> mxaccess_gateway.v1.RegisterReply
61, // 68: mxaccess_gateway.v1.MxCommandReply.add_item:type_name -> mxaccess_gateway.v1.AddItemReply
62, // 69: mxaccess_gateway.v1.MxCommandReply.add_item2:type_name -> mxaccess_gateway.v1.AddItem2Reply
@@ -8597,77 +8704,79 @@ var file_mxaccess_gateway_proto_depIdxs = []int32{
74, // 88: mxaccess_gateway.v1.MxCommandReply.session_state:type_name -> mxaccess_gateway.v1.SessionStateReply
75, // 89: mxaccess_gateway.v1.MxCommandReply.worker_info:type_name -> mxaccess_gateway.v1.WorkerInfoReply
76, // 90: mxaccess_gateway.v1.MxCommandReply.drain_events:type_name -> mxaccess_gateway.v1.DrainEventsReply
89, // 91: mxaccess_gateway.v1.SuspendReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
89, // 92: mxaccess_gateway.v1.ActivateReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
90, // 91: mxaccess_gateway.v1.SuspendReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
90, // 92: mxaccess_gateway.v1.ActivateReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
68, // 93: mxaccess_gateway.v1.BulkSubscribeReply.results:type_name -> mxaccess_gateway.v1.SubscribeResult
89, // 94: mxaccess_gateway.v1.BulkWriteResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
90, // 94: mxaccess_gateway.v1.BulkWriteResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
70, // 95: mxaccess_gateway.v1.BulkWriteReply.results:type_name -> mxaccess_gateway.v1.BulkWriteResult
90, // 96: mxaccess_gateway.v1.BulkReadResult.value:type_name -> mxaccess_gateway.v1.MxValue
102, // 97: mxaccess_gateway.v1.BulkReadResult.source_timestamp:type_name -> google.protobuf.Timestamp
89, // 98: mxaccess_gateway.v1.BulkReadResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
91, // 96: mxaccess_gateway.v1.BulkReadResult.value:type_name -> mxaccess_gateway.v1.MxValue
103, // 97: mxaccess_gateway.v1.BulkReadResult.source_timestamp:type_name -> google.protobuf.Timestamp
90, // 98: mxaccess_gateway.v1.BulkReadResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
72, // 99: mxaccess_gateway.v1.BulkReadReply.results:type_name -> mxaccess_gateway.v1.BulkReadResult
8, // 100: mxaccess_gateway.v1.SessionStateReply.state:type_name -> mxaccess_gateway.v1.SessionState
79, // 101: mxaccess_gateway.v1.DrainEventsReply.events:type_name -> mxaccess_gateway.v1.MxEvent
85, // 102: mxaccess_gateway.v1.QueryActiveAlarmsReplyPayload.snapshots:type_name -> mxaccess_gateway.v1.ActiveAlarmSnapshot
1, // 103: mxaccess_gateway.v1.MxEvent.family:type_name -> mxaccess_gateway.v1.MxEventFamily
90, // 104: mxaccess_gateway.v1.MxEvent.value:type_name -> mxaccess_gateway.v1.MxValue
102, // 105: mxaccess_gateway.v1.MxEvent.source_timestamp:type_name -> google.protobuf.Timestamp
89, // 106: mxaccess_gateway.v1.MxEvent.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
102, // 107: mxaccess_gateway.v1.MxEvent.worker_timestamp:type_name -> google.protobuf.Timestamp
102, // 108: mxaccess_gateway.v1.MxEvent.gateway_receive_timestamp:type_name -> google.protobuf.Timestamp
91, // 104: mxaccess_gateway.v1.MxEvent.value:type_name -> mxaccess_gateway.v1.MxValue
103, // 105: mxaccess_gateway.v1.MxEvent.source_timestamp:type_name -> google.protobuf.Timestamp
90, // 106: mxaccess_gateway.v1.MxEvent.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
103, // 107: mxaccess_gateway.v1.MxEvent.worker_timestamp:type_name -> google.protobuf.Timestamp
103, // 108: mxaccess_gateway.v1.MxEvent.gateway_receive_timestamp:type_name -> google.protobuf.Timestamp
80, // 109: mxaccess_gateway.v1.MxEvent.on_data_change:type_name -> mxaccess_gateway.v1.OnDataChangeEvent
81, // 110: mxaccess_gateway.v1.MxEvent.on_write_complete:type_name -> mxaccess_gateway.v1.OnWriteCompleteEvent
82, // 111: mxaccess_gateway.v1.MxEvent.operation_complete:type_name -> mxaccess_gateway.v1.OperationCompleteEvent
83, // 112: mxaccess_gateway.v1.MxEvent.on_buffered_data_change:type_name -> mxaccess_gateway.v1.OnBufferedDataChangeEvent
84, // 113: mxaccess_gateway.v1.MxEvent.on_alarm_transition:type_name -> mxaccess_gateway.v1.OnAlarmTransitionEvent
6, // 114: mxaccess_gateway.v1.OnBufferedDataChangeEvent.data_type:type_name -> mxaccess_gateway.v1.MxDataType
91, // 115: mxaccess_gateway.v1.OnBufferedDataChangeEvent.quality_values:type_name -> mxaccess_gateway.v1.MxArray
91, // 116: mxaccess_gateway.v1.OnBufferedDataChangeEvent.timestamp_values:type_name -> mxaccess_gateway.v1.MxArray
92, // 115: mxaccess_gateway.v1.OnBufferedDataChangeEvent.quality_values:type_name -> mxaccess_gateway.v1.MxArray
92, // 116: mxaccess_gateway.v1.OnBufferedDataChangeEvent.timestamp_values:type_name -> mxaccess_gateway.v1.MxArray
2, // 117: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_kind:type_name -> mxaccess_gateway.v1.AlarmTransitionKind
102, // 118: mxaccess_gateway.v1.OnAlarmTransitionEvent.original_raise_timestamp:type_name -> google.protobuf.Timestamp
102, // 119: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_timestamp:type_name -> google.protobuf.Timestamp
90, // 120: mxaccess_gateway.v1.OnAlarmTransitionEvent.current_value:type_name -> mxaccess_gateway.v1.MxValue
90, // 121: mxaccess_gateway.v1.OnAlarmTransitionEvent.limit_value:type_name -> mxaccess_gateway.v1.MxValue
102, // 122: mxaccess_gateway.v1.ActiveAlarmSnapshot.original_raise_timestamp:type_name -> google.protobuf.Timestamp
103, // 118: mxaccess_gateway.v1.OnAlarmTransitionEvent.original_raise_timestamp:type_name -> google.protobuf.Timestamp
103, // 119: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_timestamp:type_name -> google.protobuf.Timestamp
91, // 120: mxaccess_gateway.v1.OnAlarmTransitionEvent.current_value:type_name -> mxaccess_gateway.v1.MxValue
91, // 121: mxaccess_gateway.v1.OnAlarmTransitionEvent.limit_value:type_name -> mxaccess_gateway.v1.MxValue
103, // 122: mxaccess_gateway.v1.ActiveAlarmSnapshot.original_raise_timestamp:type_name -> google.protobuf.Timestamp
3, // 123: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_state:type_name -> mxaccess_gateway.v1.AlarmConditionState
102, // 124: mxaccess_gateway.v1.ActiveAlarmSnapshot.last_transition_timestamp:type_name -> google.protobuf.Timestamp
90, // 125: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_value:type_name -> mxaccess_gateway.v1.MxValue
90, // 126: mxaccess_gateway.v1.ActiveAlarmSnapshot.limit_value:type_name -> mxaccess_gateway.v1.MxValue
100, // 127: mxaccess_gateway.v1.AcknowledgeAlarmReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
89, // 128: mxaccess_gateway.v1.AcknowledgeAlarmReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
4, // 129: mxaccess_gateway.v1.MxStatusProxy.category:type_name -> mxaccess_gateway.v1.MxStatusCategory
5, // 130: mxaccess_gateway.v1.MxStatusProxy.detected_by:type_name -> mxaccess_gateway.v1.MxStatusSource
6, // 131: mxaccess_gateway.v1.MxValue.data_type:type_name -> mxaccess_gateway.v1.MxDataType
102, // 132: mxaccess_gateway.v1.MxValue.timestamp_value:type_name -> google.protobuf.Timestamp
91, // 133: mxaccess_gateway.v1.MxValue.array_value:type_name -> mxaccess_gateway.v1.MxArray
6, // 134: mxaccess_gateway.v1.MxArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
92, // 135: mxaccess_gateway.v1.MxArray.bool_values:type_name -> mxaccess_gateway.v1.BoolArray
93, // 136: mxaccess_gateway.v1.MxArray.int32_values:type_name -> mxaccess_gateway.v1.Int32Array
94, // 137: mxaccess_gateway.v1.MxArray.int64_values:type_name -> mxaccess_gateway.v1.Int64Array
95, // 138: mxaccess_gateway.v1.MxArray.float_values:type_name -> mxaccess_gateway.v1.FloatArray
96, // 139: mxaccess_gateway.v1.MxArray.double_values:type_name -> mxaccess_gateway.v1.DoubleArray
97, // 140: mxaccess_gateway.v1.MxArray.string_values:type_name -> mxaccess_gateway.v1.StringArray
98, // 141: mxaccess_gateway.v1.MxArray.timestamp_values:type_name -> mxaccess_gateway.v1.TimestampArray
99, // 142: mxaccess_gateway.v1.MxArray.raw_values:type_name -> mxaccess_gateway.v1.RawArray
102, // 143: mxaccess_gateway.v1.TimestampArray.values:type_name -> google.protobuf.Timestamp
7, // 144: mxaccess_gateway.v1.ProtocolStatus.code:type_name -> mxaccess_gateway.v1.ProtocolStatusCode
9, // 145: mxaccess_gateway.v1.MxAccessGateway.OpenSession:input_type -> mxaccess_gateway.v1.OpenSessionRequest
11, // 146: mxaccess_gateway.v1.MxAccessGateway.CloseSession:input_type -> mxaccess_gateway.v1.CloseSessionRequest
14, // 147: mxaccess_gateway.v1.MxAccessGateway.Invoke:input_type -> mxaccess_gateway.v1.MxCommandRequest
13, // 148: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:input_type -> mxaccess_gateway.v1.StreamEventsRequest
86, // 149: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:input_type -> mxaccess_gateway.v1.AcknowledgeAlarmRequest
88, // 150: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:input_type -> mxaccess_gateway.v1.QueryActiveAlarmsRequest
10, // 151: mxaccess_gateway.v1.MxAccessGateway.OpenSession:output_type -> mxaccess_gateway.v1.OpenSessionReply
12, // 152: mxaccess_gateway.v1.MxAccessGateway.CloseSession:output_type -> mxaccess_gateway.v1.CloseSessionReply
59, // 153: mxaccess_gateway.v1.MxAccessGateway.Invoke:output_type -> mxaccess_gateway.v1.MxCommandReply
79, // 154: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:output_type -> mxaccess_gateway.v1.MxEvent
87, // 155: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:output_type -> mxaccess_gateway.v1.AcknowledgeAlarmReply
85, // 156: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:output_type -> mxaccess_gateway.v1.ActiveAlarmSnapshot
151, // [151:157] is the sub-list for method output_type
145, // [145:151] is the sub-list for method input_type
145, // [145:145] is the sub-list for extension type_name
145, // [145:145] is the sub-list for extension extendee
0, // [0:145] is the sub-list for field type_name
103, // 124: mxaccess_gateway.v1.ActiveAlarmSnapshot.last_transition_timestamp:type_name -> google.protobuf.Timestamp
91, // 125: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_value:type_name -> mxaccess_gateway.v1.MxValue
91, // 126: mxaccess_gateway.v1.ActiveAlarmSnapshot.limit_value:type_name -> mxaccess_gateway.v1.MxValue
101, // 127: mxaccess_gateway.v1.AcknowledgeAlarmReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
90, // 128: mxaccess_gateway.v1.AcknowledgeAlarmReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
85, // 129: mxaccess_gateway.v1.AlarmFeedMessage.active_alarm:type_name -> mxaccess_gateway.v1.ActiveAlarmSnapshot
84, // 130: mxaccess_gateway.v1.AlarmFeedMessage.transition:type_name -> mxaccess_gateway.v1.OnAlarmTransitionEvent
4, // 131: mxaccess_gateway.v1.MxStatusProxy.category:type_name -> mxaccess_gateway.v1.MxStatusCategory
5, // 132: mxaccess_gateway.v1.MxStatusProxy.detected_by:type_name -> mxaccess_gateway.v1.MxStatusSource
6, // 133: mxaccess_gateway.v1.MxValue.data_type:type_name -> mxaccess_gateway.v1.MxDataType
103, // 134: mxaccess_gateway.v1.MxValue.timestamp_value:type_name -> google.protobuf.Timestamp
92, // 135: mxaccess_gateway.v1.MxValue.array_value:type_name -> mxaccess_gateway.v1.MxArray
6, // 136: mxaccess_gateway.v1.MxArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
93, // 137: mxaccess_gateway.v1.MxArray.bool_values:type_name -> mxaccess_gateway.v1.BoolArray
94, // 138: mxaccess_gateway.v1.MxArray.int32_values:type_name -> mxaccess_gateway.v1.Int32Array
95, // 139: mxaccess_gateway.v1.MxArray.int64_values:type_name -> mxaccess_gateway.v1.Int64Array
96, // 140: mxaccess_gateway.v1.MxArray.float_values:type_name -> mxaccess_gateway.v1.FloatArray
97, // 141: mxaccess_gateway.v1.MxArray.double_values:type_name -> mxaccess_gateway.v1.DoubleArray
98, // 142: mxaccess_gateway.v1.MxArray.string_values:type_name -> mxaccess_gateway.v1.StringArray
99, // 143: mxaccess_gateway.v1.MxArray.timestamp_values:type_name -> mxaccess_gateway.v1.TimestampArray
100, // 144: mxaccess_gateway.v1.MxArray.raw_values:type_name -> mxaccess_gateway.v1.RawArray
103, // 145: mxaccess_gateway.v1.TimestampArray.values:type_name -> google.protobuf.Timestamp
7, // 146: mxaccess_gateway.v1.ProtocolStatus.code:type_name -> mxaccess_gateway.v1.ProtocolStatusCode
9, // 147: mxaccess_gateway.v1.MxAccessGateway.OpenSession:input_type -> mxaccess_gateway.v1.OpenSessionRequest
11, // 148: mxaccess_gateway.v1.MxAccessGateway.CloseSession:input_type -> mxaccess_gateway.v1.CloseSessionRequest
14, // 149: mxaccess_gateway.v1.MxAccessGateway.Invoke:input_type -> mxaccess_gateway.v1.MxCommandRequest
13, // 150: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:input_type -> mxaccess_gateway.v1.StreamEventsRequest
86, // 151: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:input_type -> mxaccess_gateway.v1.AcknowledgeAlarmRequest
88, // 152: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:input_type -> mxaccess_gateway.v1.StreamAlarmsRequest
10, // 153: mxaccess_gateway.v1.MxAccessGateway.OpenSession:output_type -> mxaccess_gateway.v1.OpenSessionReply
12, // 154: mxaccess_gateway.v1.MxAccessGateway.CloseSession:output_type -> mxaccess_gateway.v1.CloseSessionReply
59, // 155: mxaccess_gateway.v1.MxAccessGateway.Invoke:output_type -> mxaccess_gateway.v1.MxCommandReply
79, // 156: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:output_type -> mxaccess_gateway.v1.MxEvent
87, // 157: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:output_type -> mxaccess_gateway.v1.AcknowledgeAlarmReply
89, // 158: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:output_type -> mxaccess_gateway.v1.AlarmFeedMessage
153, // [153:159] is the sub-list for method output_type
147, // [147:153] is the sub-list for method input_type
147, // [147:147] is the sub-list for extension type_name
147, // [147:147] is the sub-list for extension extendee
0, // [0:147] is the sub-list for field type_name
}
func init() { file_mxaccess_gateway_proto_init() }
@@ -8751,7 +8860,12 @@ func file_mxaccess_gateway_proto_init() {
(*MxEvent_OnAlarmTransition)(nil),
}
file_mxaccess_gateway_proto_msgTypes[78].OneofWrappers = []any{}
file_mxaccess_gateway_proto_msgTypes[81].OneofWrappers = []any{
file_mxaccess_gateway_proto_msgTypes[80].OneofWrappers = []any{
(*AlarmFeedMessage_ActiveAlarm)(nil),
(*AlarmFeedMessage_SnapshotComplete)(nil),
(*AlarmFeedMessage_Transition)(nil),
}
file_mxaccess_gateway_proto_msgTypes[82].OneofWrappers = []any{
(*MxValue_BoolValue)(nil),
(*MxValue_Int32Value)(nil),
(*MxValue_Int64Value)(nil),
@@ -8762,7 +8876,7 @@ func file_mxaccess_gateway_proto_init() {
(*MxValue_ArrayValue)(nil),
(*MxValue_RawValue)(nil),
}
file_mxaccess_gateway_proto_msgTypes[82].OneofWrappers = []any{
file_mxaccess_gateway_proto_msgTypes[83].OneofWrappers = []any{
(*MxArray_BoolValues)(nil),
(*MxArray_Int32Values)(nil),
(*MxArray_Int64Values)(nil),
@@ -8778,7 +8892,7 @@ func file_mxaccess_gateway_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mxaccess_gateway_proto_rawDesc), len(file_mxaccess_gateway_proto_rawDesc)),
NumEnums: 9,
NumMessages: 92,
NumMessages: 93,
NumExtensions: 0,
NumServices: 1,
},
@@ -19,12 +19,12 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
MxAccessGateway_AcknowledgeAlarm_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm"
MxAccessGateway_QueryActiveAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms"
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
MxAccessGateway_AcknowledgeAlarm_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm"
MxAccessGateway_StreamAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms"
)
// MxAccessGatewayClient is the client API for MxAccessGateway service.
@@ -38,7 +38,12 @@ type MxAccessGatewayClient interface {
Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error)
StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error)
AcknowledgeAlarm(ctx context.Context, in *AcknowledgeAlarmRequest, opts ...grpc.CallOption) (*AcknowledgeAlarmReply, error)
QueryActiveAlarms(ctx context.Context, in *QueryActiveAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ActiveAlarmSnapshot], error)
// 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.
StreamAlarms(ctx context.Context, in *StreamAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlarmFeedMessage], error)
}
type mxAccessGatewayClient struct {
@@ -108,13 +113,13 @@ func (c *mxAccessGatewayClient) AcknowledgeAlarm(ctx context.Context, in *Acknow
return out, nil
}
func (c *mxAccessGatewayClient) QueryActiveAlarms(ctx context.Context, in *QueryActiveAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ActiveAlarmSnapshot], error) {
func (c *mxAccessGatewayClient) StreamAlarms(ctx context.Context, in *StreamAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlarmFeedMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[1], MxAccessGateway_QueryActiveAlarms_FullMethodName, cOpts...)
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[1], MxAccessGateway_StreamAlarms_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[QueryActiveAlarmsRequest, ActiveAlarmSnapshot]{ClientStream: stream}
x := &grpc.GenericClientStream[StreamAlarmsRequest, AlarmFeedMessage]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
@@ -125,7 +130,7 @@ func (c *mxAccessGatewayClient) QueryActiveAlarms(ctx context.Context, in *Query
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MxAccessGateway_QueryActiveAlarmsClient = grpc.ServerStreamingClient[ActiveAlarmSnapshot]
type MxAccessGateway_StreamAlarmsClient = grpc.ServerStreamingClient[AlarmFeedMessage]
// MxAccessGatewayServer is the server API for MxAccessGateway service.
// All implementations must embed UnimplementedMxAccessGatewayServer
@@ -138,7 +143,12 @@ type MxAccessGatewayServer interface {
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
AcknowledgeAlarm(context.Context, *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error)
QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error
// 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.
StreamAlarms(*StreamAlarmsRequest, grpc.ServerStreamingServer[AlarmFeedMessage]) error
mustEmbedUnimplementedMxAccessGatewayServer()
}
@@ -164,8 +174,8 @@ func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grp
func (UnimplementedMxAccessGatewayServer) AcknowledgeAlarm(context.Context, *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error) {
return nil, status.Error(codes.Unimplemented, "method AcknowledgeAlarm not implemented")
}
func (UnimplementedMxAccessGatewayServer) QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error {
return status.Error(codes.Unimplemented, "method QueryActiveAlarms not implemented")
func (UnimplementedMxAccessGatewayServer) StreamAlarms(*StreamAlarmsRequest, grpc.ServerStreamingServer[AlarmFeedMessage]) error {
return status.Error(codes.Unimplemented, "method StreamAlarms not implemented")
}
func (UnimplementedMxAccessGatewayServer) mustEmbedUnimplementedMxAccessGatewayServer() {}
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
@@ -271,16 +281,16 @@ func _MxAccessGateway_AcknowledgeAlarm_Handler(srv interface{}, ctx context.Cont
return interceptor(ctx, in, info, handler)
}
func _MxAccessGateway_QueryActiveAlarms_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(QueryActiveAlarmsRequest)
func _MxAccessGateway_StreamAlarms_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(StreamAlarmsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MxAccessGatewayServer).QueryActiveAlarms(m, &grpc.GenericServerStream[QueryActiveAlarmsRequest, ActiveAlarmSnapshot]{ServerStream: stream})
return srv.(MxAccessGatewayServer).StreamAlarms(m, &grpc.GenericServerStream[StreamAlarmsRequest, AlarmFeedMessage]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type MxAccessGateway_QueryActiveAlarmsServer = grpc.ServerStreamingServer[ActiveAlarmSnapshot]
type MxAccessGateway_StreamAlarmsServer = grpc.ServerStreamingServer[AlarmFeedMessage]
// MxAccessGateway_ServiceDesc is the grpc.ServiceDesc for MxAccessGateway service.
// It's only intended for direct use with grpc.RegisterService,
@@ -313,8 +323,8 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true,
},
{
StreamName: "QueryActiveAlarms",
Handler: _MxAccessGateway_QueryActiveAlarms_Handler,
StreamName: "StreamAlarms",
Handler: _MxAccessGateway_StreamAlarms_Handler,
ServerStreams: true,
},
},
+10 -8
View File
@@ -31,22 +31,24 @@ func (c *Client) AcknowledgeAlarm(ctx context.Context, req *AcknowledgeAlarmRequ
return reply, nil
}
// QueryActiveAlarms streams a snapshot of all alarms currently Active or
// ActiveAcked — the gateway's ConditionRefresh equivalent. Used after reconnect
// to seed local Part 9 state, or to reconcile alarms that may have been missed
// during a transport blip.
// StreamAlarms attaches to the gateway's central alarm feed. The stream opens
// with one AlarmFeedMessage per currently-active alarm (the ConditionRefresh
// snapshot), then a single snapshot-complete sentinel, then a transition for
// every subsequent raise / acknowledge / clear. It is served by the gateway's
// always-on alarm monitor — no worker session is opened — so any number of
// clients may attach.
//
// The returned stream is owned by the caller; cancel ctx to release it.
// Optional alarm-reference prefix scoping (req.AlarmFilterPrefix) limits the
// stream to a sub-tree.
func (c *Client) QueryActiveAlarms(ctx context.Context, req *QueryActiveAlarmsRequest) (QueryActiveAlarmsClient, error) {
func (c *Client) StreamAlarms(ctx context.Context, req *StreamAlarmsRequest) (StreamAlarmsClient, error) {
if req == nil {
return nil, errors.New("mxgateway: query active alarms request is required")
return nil, errors.New("mxgateway: stream alarms request is required")
}
stream, err := c.raw.QueryActiveAlarms(ctx, req)
stream, err := c.raw.StreamAlarms(ctx, req)
if err != nil {
return nil, &GatewayError{Op: "query active alarms", Err: err}
return nil, &GatewayError{Op: "stream alarms", Err: err}
}
return stream, nil
+29 -30
View File
@@ -14,13 +14,11 @@ import (
"google.golang.org/grpc/test/bufconn"
)
// PR E.4 — pins the Go SDK surface for the new alarm RPCs:
// AcknowledgeAlarm + QueryActiveAlarms.
// Pins the Go SDK surface for the alarm RPCs: AcknowledgeAlarm + StreamAlarms.
func TestAcknowledgeAlarmSendsRequestAndReturnsReply(t *testing.T) {
fake := &fakeGatewayWithAlarms{
acknowledgeReply: &pb.AcknowledgeAlarmReply{
SessionId: "session-1",
CorrelationId: "corr-1",
ProtocolStatus: &pb.ProtocolStatus{
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
@@ -35,7 +33,6 @@ func TestAcknowledgeAlarmSendsRequestAndReturnsReply(t *testing.T) {
defer cleanup()
reply, err := client.AcknowledgeAlarm(context.Background(), &pb.AcknowledgeAlarmRequest{
SessionId: "session-1",
ClientCorrelationId: "corr-1",
AlarmFullReference: "Tank01.Level.HiHi",
Comment: "investigating",
@@ -77,7 +74,6 @@ func TestAcknowledgeAlarmMapsUnauthenticated(t *testing.T) {
defer cleanup()
_, err := client.AcknowledgeAlarm(context.Background(), &pb.AcknowledgeAlarmRequest{
SessionId: "session-1",
AlarmFullReference: "Tank01.Level.HiHi",
OperatorUser: "alice",
})
@@ -93,7 +89,7 @@ func TestAcknowledgeAlarmMapsUnauthenticated(t *testing.T) {
}
}
func TestQueryActiveAlarmsStreamsSnapshots(t *testing.T) {
func TestStreamAlarmsStreamsSnapshotThenSnapshotComplete(t *testing.T) {
fake := &fakeGatewayWithAlarms{
activeSnapshots: []*pb.ActiveAlarmSnapshot{
{
@@ -111,46 +107,46 @@ func TestQueryActiveAlarmsStreamsSnapshots(t *testing.T) {
client, cleanup := newBufconnClientWithAlarms(t, fake)
defer cleanup()
stream, err := client.QueryActiveAlarms(context.Background(), &pb.QueryActiveAlarmsRequest{
SessionId: "session-1",
})
stream, err := client.StreamAlarms(context.Background(), &pb.StreamAlarmsRequest{})
if err != nil {
t.Fatalf("QueryActiveAlarms() error = %v", err)
t.Fatalf("StreamAlarms() error = %v", err)
}
var received []*pb.ActiveAlarmSnapshot
var received []*pb.AlarmFeedMessage
for {
snap, err := stream.Recv()
msg, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("stream.Recv() error = %v", err)
}
received = append(received, snap)
received = append(received, msg)
}
if len(received) != 2 {
t.Fatalf("snapshot count = %d, want 2", len(received))
if len(received) != 3 {
t.Fatalf("message count = %d, want 3", len(received))
}
if received[0].GetAlarmFullReference() != "Tank01.Level.HiHi" {
t.Fatalf("snapshot[0] ref = %q", received[0].GetAlarmFullReference())
if received[0].GetActiveAlarm().GetAlarmFullReference() != "Tank01.Level.HiHi" {
t.Fatalf("message[0] ref = %q", received[0].GetActiveAlarm().GetAlarmFullReference())
}
if received[1].GetCurrentState() != pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE_ACKED {
t.Fatalf("snapshot[1] state = %v", received[1].GetCurrentState())
if received[1].GetActiveAlarm().GetCurrentState() != pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE_ACKED {
t.Fatalf("message[1] state = %v", received[1].GetActiveAlarm().GetCurrentState())
}
if !received[2].GetSnapshotComplete() {
t.Fatalf("final message is not snapshot_complete")
}
}
func TestQueryActiveAlarmsPassesFilterPrefix(t *testing.T) {
func TestStreamAlarmsPassesFilterPrefix(t *testing.T) {
fake := &fakeGatewayWithAlarms{}
client, cleanup := newBufconnClientWithAlarms(t, fake)
defer cleanup()
stream, err := client.QueryActiveAlarms(context.Background(), &pb.QueryActiveAlarmsRequest{
SessionId: "session-1",
stream, err := client.StreamAlarms(context.Background(), &pb.StreamAlarmsRequest{
AlarmFilterPrefix: "Tank01.",
})
if err != nil {
t.Fatalf("QueryActiveAlarms() error = %v", err)
t.Fatalf("StreamAlarms() error = %v", err)
}
for {
_, err := stream.Recv()
@@ -162,7 +158,7 @@ func TestQueryActiveAlarmsPassesFilterPrefix(t *testing.T) {
}
}
if got := fake.queryRequest.GetAlarmFilterPrefix(); got != "Tank01." {
if got := fake.streamRequest.GetAlarmFilterPrefix(); got != "Tank01." {
t.Fatalf("captured filter prefix = %q", got)
}
}
@@ -175,7 +171,7 @@ type fakeGatewayWithAlarms struct {
acknowledgeError error
acknowledgeAuth string
queryRequest *pb.QueryActiveAlarmsRequest
streamRequest *pb.StreamAlarmsRequest
activeSnapshots []*pb.ActiveAlarmSnapshot
}
@@ -189,21 +185,24 @@ func (s *fakeGatewayWithAlarms) AcknowledgeAlarm(ctx context.Context, req *pb.Ac
return s.acknowledgeReply, nil
}
return &pb.AcknowledgeAlarmReply{
SessionId: req.GetSessionId(),
ProtocolStatus: &pb.ProtocolStatus{
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
},
}, nil
}
func (s *fakeGatewayWithAlarms) QueryActiveAlarms(req *pb.QueryActiveAlarmsRequest, stream grpc.ServerStreamingServer[pb.ActiveAlarmSnapshot]) error {
s.queryRequest = req
func (s *fakeGatewayWithAlarms) StreamAlarms(req *pb.StreamAlarmsRequest, stream grpc.ServerStreamingServer[pb.AlarmFeedMessage]) error {
s.streamRequest = req
for _, snap := range s.activeSnapshots {
if err := stream.Send(snap); err != nil {
if err := stream.Send(&pb.AlarmFeedMessage{
Payload: &pb.AlarmFeedMessage_ActiveAlarm{ActiveAlarm: snap},
}); err != nil {
return err
}
}
return nil
return stream.Send(&pb.AlarmFeedMessage{
Payload: &pb.AlarmFeedMessage_SnapshotComplete{SnapshotComplete: true},
})
}
func newBufconnClientWithAlarms(t *testing.T, fake *fakeGatewayWithAlarms) (*Client, func()) {
+9 -6
View File
@@ -110,9 +110,12 @@ type (
AcknowledgeAlarmRequest = pb.AcknowledgeAlarmRequest
// AcknowledgeAlarmReply is the gateway AcknowledgeAlarm reply message.
AcknowledgeAlarmReply = pb.AcknowledgeAlarmReply
// QueryActiveAlarmsRequest is the gateway QueryActiveAlarms request message.
QueryActiveAlarmsRequest = pb.QueryActiveAlarmsRequest
// ActiveAlarmSnapshot is one row in a ConditionRefresh stream.
// StreamAlarmsRequest is the gateway StreamAlarms request message.
StreamAlarmsRequest = pb.StreamAlarmsRequest
// AlarmFeedMessage is one message on the StreamAlarms feed — an
// active-alarm snapshot row, a snapshot-complete sentinel, or a transition.
AlarmFeedMessage = pb.AlarmFeedMessage
// ActiveAlarmSnapshot is one currently-active alarm in the feed snapshot.
ActiveAlarmSnapshot = pb.ActiveAlarmSnapshot
// OnAlarmTransitionEvent is the body carried by alarm-transition MxEvents.
OnAlarmTransitionEvent = pb.OnAlarmTransitionEvent
@@ -126,9 +129,9 @@ type AlarmTransitionKind = pb.AlarmTransitionKind
// ConditionRefresh snapshot.
type AlarmConditionState = pb.AlarmConditionState
// QueryActiveAlarmsClient is the generated server-streaming client for the
// QueryActiveAlarms RPC.
type QueryActiveAlarmsClient = pb.MxAccessGateway_QueryActiveAlarmsClient
// StreamAlarmsClient is the generated server-streaming client for the
// StreamAlarms RPC.
type StreamAlarmsClient = pb.MxAccessGateway_StreamAlarmsClient
// Enumerations from the generated contract re-exported for client callers.
type (
@@ -5,33 +5,33 @@ import io.grpc.stub.ClientResponseObserver;
import io.grpc.stub.StreamObserver;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
/**
* Cancellable handle returned by {@code queryActiveAlarms}.
* Cancellable handle returned by {@code streamAlarms}.
*
* <p>Wraps a caller-supplied {@link StreamObserver} and exposes a
* {@link #cancel()} entry point that aborts the underlying gRPC call. The
* subscription also implements {@link AutoCloseable} so it can participate in
* try-with-resources blocks.
*/
public final class MxGatewayActiveAlarmsSubscription implements AutoCloseable {
private final AtomicReference<ClientCallStreamObserver<QueryActiveAlarmsRequest>> requestStream = new AtomicReference<>();
public final class MxGatewayAlarmFeedSubscription implements AutoCloseable {
private final AtomicReference<ClientCallStreamObserver<StreamAlarmsRequest>> requestStream = new AtomicReference<>();
private final AtomicBoolean cancelled = new AtomicBoolean();
ClientResponseObserver<QueryActiveAlarmsRequest, ActiveAlarmSnapshot> wrap(StreamObserver<ActiveAlarmSnapshot> observer) {
ClientResponseObserver<StreamAlarmsRequest, AlarmFeedMessage> wrap(StreamObserver<AlarmFeedMessage> observer) {
return new ClientResponseObserver<>() {
@Override
public void beforeStart(ClientCallStreamObserver<QueryActiveAlarmsRequest> stream) {
public void beforeStart(ClientCallStreamObserver<StreamAlarmsRequest> stream) {
requestStream.set(stream);
if (cancelled.get()) {
stream.cancel("client cancelled active-alarms query", null);
stream.cancel("client cancelled alarm feed", null);
}
}
@Override
public void onNext(ActiveAlarmSnapshot value) {
public void onNext(AlarmFeedMessage value) {
observer.onNext(value);
}
@@ -54,9 +54,9 @@ public final class MxGatewayActiveAlarmsSubscription implements AutoCloseable {
*/
public void cancel() {
cancelled.set(true);
ClientCallStreamObserver<QueryActiveAlarmsRequest> stream = requestStream.get();
ClientCallStreamObserver<StreamAlarmsRequest> stream = requestStream.get();
if (stream != null) {
stream.cancel("client cancelled active-alarms query", null);
stream.cancel("client cancelled alarm feed", null);
}
}
@@ -10,7 +10,7 @@ import java.util.concurrent.CompletableFuture;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
@@ -19,7 +19,7 @@ import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
/**
@@ -328,20 +328,24 @@ public final class MxGatewayClient implements AutoCloseable {
}
/**
* Streams a snapshot of all alarms currently Active or ActiveAcked — the
* gateway's ConditionRefresh equivalent. Used after reconnect to seed
* local Part 9 state.
* Attaches to the gateway's central alarm feed. The stream opens with one
* {@code AlarmFeedMessage} per currently-active alarm (the ConditionRefresh
* snapshot), then a single {@code snapshot_complete}, then a
* {@code transition} for every subsequent raise / acknowledge / clear.
*
* @param request the {@code QueryActiveAlarmsRequest}, optionally scoped by
* <p>Served by the gateway's always-on alarm monitor — no worker session is
* opened — so any number of clients may attach.
*
* @param request the {@code StreamAlarmsRequest}, optionally scoped by
* alarm-reference prefix
* @param observer caller-supplied observer that receives snapshots and completion
* @param observer caller-supplied observer that receives feed messages and completion
* @return a cancellable subscription handle
*/
public MxGatewayActiveAlarmsSubscription queryActiveAlarms(
QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> observer) {
MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
public MxGatewayAlarmFeedSubscription streamAlarms(
StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> observer) {
MxGatewayAlarmFeedSubscription subscription = new MxGatewayAlarmFeedSubscription();
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options)
.queryActiveAlarms(request, subscription.wrap(observer));
.streamAlarms(request, subscription.wrap(observer));
return subscription;
}
@@ -30,10 +30,11 @@ import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.AlarmConditionState;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
import org.junit.jupiter.api.Test;
@@ -57,7 +58,6 @@ final class MxGatewayLowFindingsTests {
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
seen.set(request);
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
.setSessionId(request.getSessionId())
.setProtocolStatus(ok())
.setDiagnosticMessage("acked")
.build());
@@ -67,7 +67,6 @@ final class MxGatewayLowFindingsTests {
try (Harness harness = Harness.start(service, "mxgw_keyid_secret", authorization)) {
AcknowledgeAlarmReply reply = harness.client().acknowledgeAlarm(AcknowledgeAlarmRequest.newBuilder()
.setSessionId("s-1")
.setAlarmFullReference("Area1.Pump.PV.HiHi")
.setComment("operator note")
.build());
@@ -84,7 +83,6 @@ final class MxGatewayLowFindingsTests {
public void acknowledgeAlarm(
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
.setSessionId(request.getSessionId())
.setProtocolStatus(ProtocolStatus.newBuilder()
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND))
.build());
@@ -96,7 +94,7 @@ final class MxGatewayLowFindingsTests {
assertThrows(
MxGatewayException.class,
() -> harness.client().acknowledgeAlarm(AcknowledgeAlarmRequest.newBuilder()
.setSessionId("missing")
.setAlarmFullReference("Area1.Pump.PV.HiHi")
.build()));
}
}
@@ -108,7 +106,6 @@ final class MxGatewayLowFindingsTests {
public void acknowledgeAlarm(
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
.setSessionId(request.getSessionId())
.setProtocolStatus(ok())
.setDiagnosticMessage("async-acked")
.build());
@@ -118,7 +115,9 @@ final class MxGatewayLowFindingsTests {
try (Harness harness = Harness.start(service)) {
CompletableFuture<AcknowledgeAlarmReply> future = harness.client()
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder().setSessionId("s-2").build());
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder()
.setAlarmFullReference("Area1.Pump.PV.HiHi")
.build());
assertEquals("async-acked", future.get(5, TimeUnit.SECONDS).getDiagnosticMessage());
}
}
@@ -135,39 +134,45 @@ final class MxGatewayLowFindingsTests {
try (Harness harness = Harness.start(service)) {
CompletableFuture<AcknowledgeAlarmReply> future = harness.client()
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder().setSessionId("s-3").build());
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder()
.setAlarmFullReference("Area1.Pump.PV.HiHi")
.build());
ExecutionException error = assertThrows(
ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
assertTrue(error.getCause() instanceof MxGatewayException, () -> String.valueOf(error.getCause()));
}
}
// --- Client.Java-007: QueryActiveAlarms RPC + subscription coverage ---
// --- Client.Java-007: StreamAlarms RPC + subscription coverage ---
@Test
void queryActiveAlarmsDeliversSnapshotsToObserver() throws Exception {
ActiveAlarmSnapshot snapshot = ActiveAlarmSnapshot.newBuilder()
.setAlarmFullReference("Area1.Tank.Level.Hi")
.setSeverity(800)
.setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE)
void streamAlarmsDeliversFeedMessagesToObserver() throws Exception {
AlarmFeedMessage active = AlarmFeedMessage.newBuilder()
.setActiveAlarm(ActiveAlarmSnapshot.newBuilder()
.setAlarmFullReference("Area1.Tank.Level.Hi")
.setSeverity(800)
.setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE))
.build();
AlarmFeedMessage snapshotComplete =
AlarmFeedMessage.newBuilder().setSnapshotComplete(true).build();
TestService service = new TestService() {
@Override
public void queryActiveAlarms(
QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> responseObserver) {
responseObserver.onNext(snapshot);
public void streamAlarms(
StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> responseObserver) {
responseObserver.onNext(active);
responseObserver.onNext(snapshotComplete);
responseObserver.onCompleted();
}
};
try (Harness harness = Harness.start(service)) {
List<ActiveAlarmSnapshot> received = new ArrayList<>();
List<AlarmFeedMessage> received = new ArrayList<>();
CountDownLatch done = new CountDownLatch(1);
harness.client().queryActiveAlarms(
QueryActiveAlarmsRequest.newBuilder().setSessionId("s-4").build(),
harness.client().streamAlarms(
StreamAlarmsRequest.newBuilder().build(),
new StreamObserver<>() {
@Override
public void onNext(ActiveAlarmSnapshot value) {
public void onNext(AlarmFeedMessage value) {
received.add(value);
}
@@ -182,18 +187,19 @@ final class MxGatewayLowFindingsTests {
}
});
assertTrue(done.await(5, TimeUnit.SECONDS), "stream should complete");
assertEquals(1, received.size());
assertEquals("Area1.Tank.Level.Hi", received.get(0).getAlarmFullReference());
assertEquals(2, received.size());
assertEquals("Area1.Tank.Level.Hi", received.get(0).getActiveAlarm().getAlarmFullReference());
assertTrue(received.get(1).getSnapshotComplete());
}
}
@Test
void activeAlarmsSubscriptionCancelBeforeBeforeStartCancelsStream() {
MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
ClientResponseObserver<QueryActiveAlarmsRequest, ActiveAlarmSnapshot> observer =
void alarmFeedSubscriptionCancelBeforeBeforeStartCancelsStream() {
MxGatewayAlarmFeedSubscription subscription = new MxGatewayAlarmFeedSubscription();
ClientResponseObserver<StreamAlarmsRequest, AlarmFeedMessage> observer =
subscription.wrap(new StreamObserver<>() {
@Override
public void onNext(ActiveAlarmSnapshot value) {
public void onNext(AlarmFeedMessage value) {
}
@Override
@@ -204,13 +210,13 @@ final class MxGatewayLowFindingsTests {
public void onCompleted() {
}
});
RecordingActiveAlarmsRequestStream requestStream = new RecordingActiveAlarmsRequestStream();
RecordingAlarmFeedRequestStream requestStream = new RecordingAlarmFeedRequestStream();
subscription.cancel();
observer.beforeStart(requestStream);
assertTrue(requestStream.cancelled);
assertEquals("client cancelled active-alarms query", requestStream.cancelMessage);
assertEquals("client cancelled alarm feed", requestStream.cancelMessage);
}
// --- Client.Java-007: async streamEvents + subscription cancellation ---
@@ -456,8 +462,8 @@ final class MxGatewayLowFindingsTests {
}
}
private static final class RecordingActiveAlarmsRequestStream
extends ClientCallStreamObserver<QueryActiveAlarmsRequest> {
private static final class RecordingAlarmFeedRequestStream
extends ClientCallStreamObserver<StreamAlarmsRequest> {
private boolean cancelled;
private String cancelMessage;
@@ -489,7 +495,7 @@ final class MxGatewayLowFindingsTests {
}
@Override
public void onNext(QueryActiveAlarmsRequest value) {
public void onNext(StreamAlarmsRequest value) {
}
@Override
@@ -170,35 +170,35 @@ public final class MxAccessGatewayGrpc {
return getAcknowledgeAlarmMethod;
}
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod;
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> getStreamAlarmsMethod;
@io.grpc.stub.annotations.RpcMethod(
fullMethodName = SERVICE_NAME + '/' + "QueryActiveAlarms",
requestType = mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest.class,
responseType = mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot.class,
fullMethodName = SERVICE_NAME + '/' + "StreamAlarms",
requestType = mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest.class,
responseType = mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage.class,
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod() {
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod;
if ((getQueryActiveAlarmsMethod = MxAccessGatewayGrpc.getQueryActiveAlarmsMethod) == null) {
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> getStreamAlarmsMethod() {
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> getStreamAlarmsMethod;
if ((getStreamAlarmsMethod = MxAccessGatewayGrpc.getStreamAlarmsMethod) == null) {
synchronized (MxAccessGatewayGrpc.class) {
if ((getQueryActiveAlarmsMethod = MxAccessGatewayGrpc.getQueryActiveAlarmsMethod) == null) {
MxAccessGatewayGrpc.getQueryActiveAlarmsMethod = getQueryActiveAlarmsMethod =
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>newBuilder()
if ((getStreamAlarmsMethod = MxAccessGatewayGrpc.getStreamAlarmsMethod) == null) {
MxAccessGatewayGrpc.getStreamAlarmsMethod = getStreamAlarmsMethod =
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage>newBuilder()
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "QueryActiveAlarms"))
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAlarms"))
.setSampledToLocalTracing(true)
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest.getDefaultInstance()))
mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest.getDefaultInstance()))
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot.getDefaultInstance()))
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("QueryActiveAlarms"))
mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage.getDefaultInstance()))
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("StreamAlarms"))
.build();
}
}
}
return getQueryActiveAlarmsMethod;
return getStreamAlarmsMethod;
}
/**
@@ -303,10 +303,17 @@ public final class MxAccessGatewayGrpc {
}
/**
* <pre>
* 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.
* </pre>
*/
default void queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request,
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> responseObserver) {
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getQueryActiveAlarmsMethod(), responseObserver);
default void streamAlarms(mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest request,
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> responseObserver) {
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamAlarmsMethod(), responseObserver);
}
}
@@ -384,11 +391,18 @@ public final class MxAccessGatewayGrpc {
}
/**
* <pre>
* 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.
* </pre>
*/
public void queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request,
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> responseObserver) {
public void streamAlarms(mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest request,
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> responseObserver) {
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
getChannel().newCall(getQueryActiveAlarmsMethod(), getCallOptions()), request, responseObserver);
getChannel().newCall(getStreamAlarmsMethod(), getCallOptions()), request, responseObserver);
}
}
@@ -449,12 +463,19 @@ public final class MxAccessGatewayGrpc {
}
/**
* <pre>
* 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.
* </pre>
*/
@io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918")
public io.grpc.stub.BlockingClientCall<?, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>
queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request) {
public io.grpc.stub.BlockingClientCall<?, mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage>
streamAlarms(mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest request) {
return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall(
getChannel(), getQueryActiveAlarmsMethod(), getCallOptions(), request);
getChannel(), getStreamAlarmsMethod(), getCallOptions(), request);
}
}
@@ -514,11 +535,18 @@ public final class MxAccessGatewayGrpc {
}
/**
* <pre>
* 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.
* </pre>
*/
public java.util.Iterator<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> queryActiveAlarms(
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request) {
public java.util.Iterator<mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage> streamAlarms(
mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest request) {
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
getChannel(), getQueryActiveAlarmsMethod(), getCallOptions(), request);
getChannel(), getStreamAlarmsMethod(), getCallOptions(), request);
}
}
@@ -579,7 +607,7 @@ public final class MxAccessGatewayGrpc {
private static final int METHODID_INVOKE = 2;
private static final int METHODID_STREAM_EVENTS = 3;
private static final int METHODID_ACKNOWLEDGE_ALARM = 4;
private static final int METHODID_QUERY_ACTIVE_ALARMS = 5;
private static final int METHODID_STREAM_ALARMS = 5;
private static final class MethodHandlers<Req, Resp> implements
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
@@ -618,9 +646,9 @@ public final class MxAccessGatewayGrpc {
serviceImpl.acknowledgeAlarm((mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest) request,
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply>) responseObserver);
break;
case METHODID_QUERY_ACTIVE_ALARMS:
serviceImpl.queryActiveAlarms((mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest) request,
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>) responseObserver);
case METHODID_STREAM_ALARMS:
serviceImpl.streamAlarms((mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest) request,
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage>) responseObserver);
break;
default:
throw new AssertionError();
@@ -676,12 +704,12 @@ public final class MxAccessGatewayGrpc {
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply>(
service, METHODID_ACKNOWLEDGE_ALARM)))
.addMethod(
getQueryActiveAlarmsMethod(),
getStreamAlarmsMethod(),
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
new MethodHandlers<
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>(
service, METHODID_QUERY_ACTIVE_ALARMS)))
mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest,
mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage>(
service, METHODID_STREAM_ALARMS)))
.build();
}
@@ -735,7 +763,7 @@ public final class MxAccessGatewayGrpc {
.addMethod(getInvokeMethod())
.addMethod(getStreamEventsMethod())
.addMethod(getAcknowledgeAlarmMethod())
.addMethod(getQueryActiveAlarmsMethod())
.addMethod(getStreamAlarmsMethod())
.build();
}
}
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -166,25 +166,27 @@ class GatewayClient:
ensure_protocol_success("acknowledge alarm", reply.protocol_status, reply)
return reply
def query_active_alarms(
def stream_alarms(
self,
request: pb.QueryActiveAlarmsRequest,
request: pb.StreamAlarmsRequest,
*,
metadata: Sequence[tuple[str, str]] | None = None,
) -> AsyncIterator[pb.ActiveAlarmSnapshot]:
"""Stream a snapshot of all alarms currently Active or ActiveAcked.
) -> AsyncIterator[pb.AlarmFeedMessage]:
"""Attach to the gateway's central alarm feed.
The gateway's ConditionRefresh equivalent. Use after reconnect to seed
local Part 9 state, or to reconcile alarms that may have been missed
during a transport blip. Optionally scoped by alarm-reference prefix
(``request.alarm_filter_prefix``) so a partial refresh can target an
equipment sub-tree.
The stream opens with one ``AlarmFeedMessage`` per currently-active
alarm (the ConditionRefresh snapshot), then a single
``snapshot_complete``, then a ``transition`` for every subsequent
raise / acknowledge / clear. Served by the gateway's always-on alarm
monitor — no worker session is opened — so any number of clients may
attach. Optionally scoped by alarm-reference prefix
(``request.alarm_filter_prefix``).
"""
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
if self.options.stream_timeout is not None:
kwargs["timeout"] = self.options.stream_timeout
call = _open_stream(self.raw_stub.QueryActiveAlarms, request, kwargs)
return _canceling_iterator(call, "query active alarms")
call = _open_stream(self.raw_stub.StreamAlarms, request, kwargs)
return _canceling_iterator(call, "stream alarms")
async def _unary(
self,
File diff suppressed because one or more lines are too long
@@ -30,8 +30,7 @@ class MxAccessGatewayStub(object):
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.
cannot accidentally reuse the retired tag.
Public client API for MXAccess sessions hosted by the gateway.
"""
@@ -67,10 +66,10 @@ class MxAccessGatewayStub(object):
request_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.SerializeToString,
response_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.FromString,
_registered_method=True)
self.QueryActiveAlarms = channel.unary_stream(
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
request_serializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
response_deserializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
self.StreamAlarms = channel.unary_stream(
'/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms',
request_serializer=mxaccess__gateway__pb2.StreamAlarmsRequest.SerializeToString,
response_deserializer=mxaccess__gateway__pb2.AlarmFeedMessage.FromString,
_registered_method=True)
@@ -79,8 +78,7 @@ class MxAccessGatewayServicer(object):
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.
cannot accidentally reuse the retired tag.
Public client API for MXAccess sessions hosted by the gateway.
"""
@@ -115,8 +113,13 @@ class MxAccessGatewayServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def QueryActiveAlarms(self, request, context):
"""Missing associated documentation comment in .proto file."""
def StreamAlarms(self, request, context):
"""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.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
@@ -149,10 +152,10 @@ def add_MxAccessGatewayServicer_to_server(servicer, server):
request_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.FromString,
response_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.SerializeToString,
),
'QueryActiveAlarms': grpc.unary_stream_rpc_method_handler(
servicer.QueryActiveAlarms,
request_deserializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.FromString,
response_serializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.SerializeToString,
'StreamAlarms': grpc.unary_stream_rpc_method_handler(
servicer.StreamAlarms,
request_deserializer=mxaccess__gateway__pb2.StreamAlarmsRequest.FromString,
response_serializer=mxaccess__gateway__pb2.AlarmFeedMessage.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
@@ -167,8 +170,7 @@ class MxAccessGateway(object):
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.
cannot accidentally reuse the retired tag.
Public client API for MXAccess sessions hosted by the gateway.
"""
@@ -309,7 +311,7 @@ class MxAccessGateway(object):
_registered_method=True)
@staticmethod
def QueryActiveAlarms(request,
def StreamAlarms(request,
target,
options=(),
channel_credentials=None,
@@ -322,9 +324,9 @@ class MxAccessGateway(object):
return grpc.experimental.unary_stream(
request,
target,
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
'/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms',
mxaccess__gateway__pb2.StreamAlarmsRequest.SerializeToString,
mxaccess__gateway__pb2.AlarmFeedMessage.FromString,
options,
channel_credentials,
insecure,
+62 -61
View File
@@ -1,8 +1,7 @@
"""Tests for the AcknowledgeAlarm + QueryActiveAlarms client surface (PR E.3)."""
"""Tests for the AcknowledgeAlarm + StreamAlarms client surface."""
from __future__ import annotations
import asyncio
from typing import Any
import grpc
@@ -18,7 +17,6 @@ async def test_acknowledge_alarm_sends_request_and_returns_reply() -> None:
stub = FakeGatewayStub()
stub.acknowledge_alarm.replies = [
pb.AcknowledgeAlarmReply(
session_id="session-1",
correlation_id="corr-7",
protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
status=pb.MxStatusProxy(success=1, category=pb.MX_STATUS_CATEGORY_OK),
@@ -31,7 +29,6 @@ async def test_acknowledge_alarm_sends_request_and_returns_reply() -> None:
reply = await client.acknowledge_alarm(
pb.AcknowledgeAlarmRequest(
session_id="session-1",
client_correlation_id="corr-7",
alarm_full_reference="Tank01.Level.HiHi",
comment="investigating",
@@ -61,7 +58,6 @@ async def test_acknowledge_alarm_unauthenticated_raises_typed_error() -> None:
with pytest.raises(MxGatewayAuthenticationError):
await client.acknowledge_alarm(
pb.AcknowledgeAlarmRequest(
session_id="session-1",
alarm_full_reference="Tank01.Level.HiHi",
comment="",
operator_user="alice",
@@ -81,7 +77,6 @@ async def test_acknowledge_alarm_permission_denied_raises_typed_error() -> None:
with pytest.raises(MxGatewayAuthorizationError):
await client.acknowledge_alarm(
pb.AcknowledgeAlarmRequest(
session_id="session-1",
alarm_full_reference="Tank01.Level.HiHi",
comment="",
operator_user="alice",
@@ -90,84 +85,90 @@ async def test_acknowledge_alarm_permission_denied_raises_typed_error() -> None:
@pytest.mark.asyncio
async def test_query_active_alarms_streams_snapshots() -> None:
snapshots = [
pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank01.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE,
severity=750,
async def test_stream_alarms_streams_snapshot_then_snapshot_complete() -> None:
messages = [
pb.AlarmFeedMessage(
active_alarm=pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank01.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE,
severity=750,
),
),
pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank02.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE_ACKED,
severity=750,
pb.AlarmFeedMessage(
active_alarm=pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank02.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE_ACKED,
severity=750,
),
),
pb.AlarmFeedMessage(snapshot_complete=True),
]
stream = FakeSnapshotStream(snapshots)
stub = FakeGatewayStub(snapshot_stream=stream)
stream = FakeAlarmFeedStream(messages)
stub = FakeGatewayStub(alarm_feed_stream=stream)
client = await GatewayClient.connect(
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
stub=stub,
)
received: list[pb.ActiveAlarmSnapshot] = []
async for snapshot in client.query_active_alarms(
pb.QueryActiveAlarmsRequest(session_id="session-1"),
):
received.append(snapshot)
received: list[pb.AlarmFeedMessage] = []
async for message in client.stream_alarms(pb.StreamAlarmsRequest()):
received.append(message)
assert len(received) == 2
assert received[0].alarm_full_reference == "Tank01.Level.HiHi"
assert received[0].current_state == pb.ALARM_CONDITION_STATE_ACTIVE
assert received[1].current_state == pb.ALARM_CONDITION_STATE_ACTIVE_ACKED
assert stub.query_metadata == (("authorization", "Bearer mxgw_test_secret"),)
assert len(received) == 3
assert received[0].active_alarm.alarm_full_reference == "Tank01.Level.HiHi"
assert received[0].active_alarm.current_state == pb.ALARM_CONDITION_STATE_ACTIVE
assert received[1].active_alarm.current_state == pb.ALARM_CONDITION_STATE_ACTIVE_ACKED
assert received[2].snapshot_complete is True
assert stub.stream_metadata == (("authorization", "Bearer mxgw_test_secret"),)
@pytest.mark.asyncio
async def test_query_active_alarms_passes_filter_prefix() -> None:
stream = FakeSnapshotStream([])
stub = FakeGatewayStub(snapshot_stream=stream)
async def test_stream_alarms_passes_filter_prefix() -> None:
stream = FakeAlarmFeedStream([])
stub = FakeGatewayStub(alarm_feed_stream=stream)
client = await GatewayClient.connect(
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
stub=stub,
)
iterator = client.query_active_alarms(
pb.QueryActiveAlarmsRequest(session_id="session-1", alarm_filter_prefix="Tank01."),
iterator = client.stream_alarms(
pb.StreamAlarmsRequest(alarm_filter_prefix="Tank01."),
)
# Drain to trigger the stub call.
async for _ in iterator:
pass
assert stub.query_request is not None
assert stub.query_request.alarm_filter_prefix == "Tank01."
assert stub.stream_request is not None
assert stub.stream_request.alarm_filter_prefix == "Tank01."
@pytest.mark.asyncio
async def test_query_active_alarms_cancels_underlying_stream_on_close() -> None:
snapshots = [
pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank01.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE,
async def test_stream_alarms_cancels_underlying_stream_on_close() -> None:
messages = [
pb.AlarmFeedMessage(
active_alarm=pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank01.Level.HiHi",
current_state=pb.ALARM_CONDITION_STATE_ACTIVE,
),
),
]
stream = FakeSnapshotStream(snapshots)
stub = FakeGatewayStub(snapshot_stream=stream)
stream = FakeAlarmFeedStream(messages)
stub = FakeGatewayStub(alarm_feed_stream=stream)
client = await GatewayClient.connect(
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
stub=stub,
)
iterator = client.query_active_alarms(pb.QueryActiveAlarmsRequest(session_id="session-1"))
iterator = client.stream_alarms(pb.StreamAlarmsRequest())
first = await anext(iterator)
await iterator.aclose()
assert first.alarm_full_reference == "Tank01.Level.HiHi"
assert first.active_alarm.alarm_full_reference == "Tank01.Level.HiHi"
assert stream.cancelled
class FakeGatewayStub:
def __init__(self, snapshot_stream: "FakeSnapshotStream | None" = None) -> None:
def __init__(self, alarm_feed_stream: "FakeAlarmFeedStream | None" = None) -> None:
self.open_session = FakeUnary(
[
pb.OpenSessionReply(
@@ -179,19 +180,19 @@ class FakeGatewayStub:
self.acknowledge_alarm = FakeUnary([])
self.OpenSession = self.open_session
self.AcknowledgeAlarm = self.acknowledge_alarm
self._snapshot_stream = snapshot_stream or FakeSnapshotStream([])
self.query_request: pb.QueryActiveAlarmsRequest | None = None
self.query_metadata: tuple[tuple[str, str], ...] | None = None
self._alarm_feed_stream = alarm_feed_stream or FakeAlarmFeedStream([])
self.stream_request: pb.StreamAlarmsRequest | None = None
self.stream_metadata: tuple[tuple[str, str], ...] | None = None
def QueryActiveAlarms(
def StreamAlarms(
self,
request: pb.QueryActiveAlarmsRequest,
request: pb.StreamAlarmsRequest,
*,
metadata: tuple[tuple[str, str], ...],
) -> "FakeSnapshotStream":
self.query_request = request
self.query_metadata = metadata
return self._snapshot_stream
) -> "FakeAlarmFeedStream":
self.stream_request = request
self.stream_metadata = metadata
return self._alarm_feed_stream
class FakeUnary:
@@ -214,18 +215,18 @@ class FakeUnary:
return self.replies.pop(0)
class FakeSnapshotStream:
def __init__(self, snapshots: list[pb.ActiveAlarmSnapshot]) -> None:
self._snapshots = list(snapshots)
class FakeAlarmFeedStream:
def __init__(self, messages: list[pb.AlarmFeedMessage]) -> None:
self._messages = list(messages)
self.cancelled = False
def __aiter__(self) -> "FakeSnapshotStream":
def __aiter__(self) -> "FakeAlarmFeedStream":
return self
async def __anext__(self) -> pb.ActiveAlarmSnapshot:
if not self._snapshots:
async def __anext__(self) -> pb.AlarmFeedMessage:
if not self._messages:
raise StopAsyncIteration
return self._snapshots.pop(0)
return self._messages.pop(0)
def cancel(self) -> None:
self.cancelled = True
@@ -1,6 +1,6 @@
"""Regression tests for Client.Python-003: stream timeout-kwarg fallback.
`stream_events_raw` and `query_active_alarms` must tolerate a fake/older stub
`stream_events_raw` and `stream_alarms` must tolerate a fake/older stub
that does not accept a ``timeout`` keyword argument, matching the fallback
already present in `galaxy.watch_deploy_events` and the unary `_unary` helper.
"""
@@ -51,9 +51,9 @@ class _NoTimeoutStubStreamEvents:
self.StreamEvents = stream
class _NoTimeoutStubQueryAlarms:
class _NoTimeoutStubStreamAlarms:
def __init__(self, stream: _NoTimeoutStream) -> None:
self.QueryActiveAlarms = stream
self.StreamAlarms = stream
@pytest.mark.asyncio
@@ -78,24 +78,30 @@ async def test_stream_events_raw_falls_back_when_stub_rejects_timeout() -> None:
@pytest.mark.asyncio
async def test_query_active_alarms_falls_back_when_stub_rejects_timeout() -> None:
async def test_stream_alarms_falls_back_when_stub_rejects_timeout() -> None:
stream = _NoTimeoutStream(
[pb.ActiveAlarmSnapshot(alarm_full_reference="Tank01.Level.HiHi")],
[
pb.AlarmFeedMessage(
active_alarm=pb.ActiveAlarmSnapshot(
alarm_full_reference="Tank01.Level.HiHi",
),
),
],
)
client = await GatewayClient.connect(
ClientOptions(endpoint="fake", plaintext=True, stream_timeout=5.0),
stub=_NoTimeoutStubQueryAlarms(stream),
stub=_NoTimeoutStubStreamAlarms(stream),
)
received = [
snapshot
async for snapshot in client.query_active_alarms(
pb.QueryActiveAlarmsRequest(session_id="session-1"),
message
async for message in client.stream_alarms(
pb.StreamAlarmsRequest(),
)
]
assert len(received) == 1
assert received[0].alarm_full_reference == "Tank01.Level.HiHi"
assert received[0].active_alarm.alarm_full_reference == "Tank01.Level.HiHi"
@pytest.mark.asyncio
+19 -18
View File
@@ -16,9 +16,9 @@ use crate::auth::AuthInterceptor;
use crate::error::{ensure_command_success, ensure_protocol_success, Error};
use crate::generated::mxaccess_gateway::v1::mx_access_gateway_client::MxAccessGatewayClient;
use crate::generated::mxaccess_gateway::v1::{
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, CloseSessionReply,
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, AlarmFeedMessage, CloseSessionReply,
CloseSessionRequest, MxCommandReply, MxCommandRequest, MxEvent, OpenSessionReply,
OpenSessionRequest, QueryActiveAlarmsRequest, StreamEventsRequest,
OpenSessionRequest, StreamAlarmsRequest, StreamEventsRequest,
};
use crate::options::ClientOptions;
use crate::session::Session;
@@ -33,11 +33,11 @@ pub type RawGatewayClient = MxAccessGatewayClient<InterceptedService<Channel, Au
pub type EventStream =
std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>;
/// Pinned, boxed [`ActiveAlarmSnapshot`] stream returned by
/// [`GatewayClient::query_active_alarms`]. Errors are pre-mapped from
/// Pinned, boxed [`AlarmFeedMessage`] stream returned by
/// [`GatewayClient::stream_alarms`]. Errors are pre-mapped from
/// `tonic::Status` to [`Error`]; dropping the stream cancels the call.
pub type ActiveAlarmStream = std::pin::Pin<
Box<dyn futures_core::Stream<Item = Result<ActiveAlarmSnapshot, Error>> + Send + 'static>,
pub type AlarmFeedStream = std::pin::Pin<
Box<dyn futures_core::Stream<Item = Result<AlarmFeedMessage, Error>> + Send + 'static>,
>;
/// Thin async wrapper around the generated gateway client.
@@ -227,26 +227,27 @@ impl GatewayClient {
Ok(reply)
}
/// Open the server-streaming `QueryActiveAlarms` RPC — the gateway's
/// ConditionRefresh equivalent.
/// Attach to the gateway's central `StreamAlarms` feed.
///
/// The returned [`ActiveAlarmStream`] yields one [`ActiveAlarmSnapshot`]
/// per currently-active alarm. Dropping the stream cancels the gRPC call
/// cooperatively. Optional alarm-reference prefix scoping
/// (`request.alarm_filter_prefix`) limits the stream to a sub-tree.
/// The returned [`AlarmFeedStream`] opens with one [`AlarmFeedMessage`]
/// per currently-active alarm (the ConditionRefresh snapshot), then a
/// single `snapshot_complete`, then a `transition` for every subsequent
/// raise / acknowledge / clear. It is served by the gateway's always-on
/// alarm monitor — no worker session is opened — so any number of clients
/// may attach. Dropping the stream cancels the gRPC call cooperatively.
/// Optional alarm-reference prefix scoping (`request.alarm_filter_prefix`)
/// limits the stream to a sub-tree.
///
/// # Errors
///
/// Returns the `tonic::Status` mapped through [`Error::from`] if the
/// server rejects the request.
pub async fn query_active_alarms(
pub async fn stream_alarms(
&self,
request: QueryActiveAlarmsRequest,
) -> Result<ActiveAlarmStream, Error> {
request: StreamAlarmsRequest,
) -> Result<AlarmFeedStream, Error> {
let mut client = self.inner.clone();
let response = client
.query_active_alarms(self.stream_request(request))
.await?;
let response = client.stream_alarms(self.stream_request(request)).await?;
let stream = futures_util::StreamExt::map(response.into_inner(), |result| {
result.map_err(Error::from)
});
+39 -22
View File
@@ -8,6 +8,7 @@ use std::time::Duration;
use futures_core::Stream;
use futures_util::StreamExt;
use mxgateway_client::generated::mxaccess_gateway::v1::alarm_feed_message;
use mxgateway_client::generated::mxaccess_gateway::v1::mx_access_gateway_server::{
MxAccessGateway, MxAccessGatewayServer,
};
@@ -16,12 +17,12 @@ use mxgateway_client::generated::mxaccess_gateway::v1::mx_command_reply;
use mxgateway_client::generated::mxaccess_gateway::v1::mx_value::Kind;
use mxgateway_client::generated::mxaccess_gateway::v1::{
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, AddItemReply,
BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply, BulkWriteResult,
CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply, MxDataType, MxEvent,
MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue, OpenSessionReply,
OpenSessionRequest, ProtocolStatus, ProtocolStatusCode, QueryActiveAlarmsRequest, SessionState,
StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
WriteSecuredBulkEntry,
AlarmFeedMessage, BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply,
BulkWriteResult, CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply,
MxDataType, MxEvent, MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
OpenSessionReply, OpenSessionRequest, ProtocolStatus, ProtocolStatusCode, SessionState,
StreamAlarmsRequest, StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry,
WriteSecured2BulkEntry, WriteSecuredBulkEntry,
};
use mxgateway_client::{
ApiKey, ClientOptions, CommandError, Error, GatewayClient, MxStatus, MxValue as ClientMxValue,
@@ -208,7 +209,6 @@ async fn acknowledge_alarm_returns_reply_with_native_status() {
let reply = client
.acknowledge_alarm(AcknowledgeAlarmRequest {
session_id: "session-fixture".to_owned(),
client_correlation_id: "corr-1".to_owned(),
alarm_full_reference: "Tank01.Level.HiHi".to_owned(),
comment: "investigating".to_owned(),
@@ -225,7 +225,7 @@ async fn acknowledge_alarm_returns_reply_with_native_status() {
}
#[tokio::test]
async fn query_active_alarms_streams_snapshot_rows() {
async fn stream_alarms_streams_snapshot_then_complete() {
let state = Arc::new(FakeState::default());
let endpoint = spawn_fake_gateway(state.clone()).await;
let client = GatewayClient::connect(ClientOptions::new(endpoint))
@@ -233,15 +233,23 @@ async fn query_active_alarms_streams_snapshot_rows() {
.unwrap();
let mut stream = client
.query_active_alarms(QueryActiveAlarmsRequest {
session_id: "session-fixture".to_owned(),
..QueryActiveAlarmsRequest::default()
})
.stream_alarms(StreamAlarmsRequest::default())
.await
.unwrap();
let first = stream.next().await.unwrap().unwrap();
assert_eq!(first.alarm_full_reference, "Tank01.Level.HiHi");
match first.payload {
Some(alarm_feed_message::Payload::ActiveAlarm(snapshot)) => {
assert_eq!(snapshot.alarm_full_reference, "Tank01.Level.HiHi");
}
other => panic!("expected an active-alarm snapshot, got {other:?}"),
}
let second = stream.next().await.unwrap().unwrap();
assert_eq!(
second.payload,
Some(alarm_feed_message::Payload::SnapshotComplete(true)),
);
}
#[test]
@@ -907,7 +915,6 @@ impl MxAccessGateway for FakeGateway {
_request: Request<AcknowledgeAlarmRequest>,
) -> Result<Response<AcknowledgeAlarmReply>, Status> {
Ok(Response::new(AcknowledgeAlarmReply {
session_id: "session-fixture".to_owned(),
correlation_id: "corr-1".to_owned(),
protocol_status: Some(ok_status("ack ok")),
status: Some(MxStatusProxy {
@@ -920,18 +927,28 @@ impl MxAccessGateway for FakeGateway {
}))
}
type QueryActiveAlarmsStream =
Pin<Box<dyn Stream<Item = Result<ActiveAlarmSnapshot, Status>> + Send + 'static>>;
type StreamAlarmsStream =
Pin<Box<dyn Stream<Item = Result<AlarmFeedMessage, Status>> + Send + 'static>>;
async fn query_active_alarms(
async fn stream_alarms(
&self,
_request: Request<QueryActiveAlarmsRequest>,
) -> Result<Response<Self::QueryActiveAlarmsStream>, Status> {
_request: Request<StreamAlarmsRequest>,
) -> Result<Response<Self::StreamAlarmsStream>, Status> {
let (sender, receiver) = mpsc::channel(4);
sender
.send(Ok(ActiveAlarmSnapshot {
alarm_full_reference: "Tank01.Level.HiHi".to_owned(),
..ActiveAlarmSnapshot::default()
.send(Ok(AlarmFeedMessage {
payload: Some(alarm_feed_message::Payload::ActiveAlarm(
ActiveAlarmSnapshot {
alarm_full_reference: "Tank01.Level.HiHi".to_owned(),
..ActiveAlarmSnapshot::default()
},
)),
}))
.await
.unwrap();
sender
.send(Ok(AlarmFeedMessage {
payload: Some(alarm_feed_message::Payload::SnapshotComplete(true)),
}))
.await
.unwrap();
-828
View File
@@ -1,828 +0,0 @@
# aaAlarmManagedClient discovery — public surface, 2026-05-01
Result of running
`MxGateway.Worker.Tests.AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface`
against the deployed AVEVA assembly:
- File:
`C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll`
- Assembly identity: `aaAlarmManagedClient, Version=1.0.7368.41290,
Culture=neutral, PublicKeyToken=7ebd82b507d9e10c`
## Public types
- `aaAlarmManagedClient.AlarmClient` (class)
- `aaAlarmManagedClient.PriorityData` (class)
That's the entire exported surface — two types, no interfaces, no
delegates.
## `AlarmClient` events
**None.** The class has no public events at all. The reflection probe's
`GetEvents(BindingFlags.Public | Instance | Static)` returned an empty
list.
## `AlarmClient` methods (relevant subset)
- **Lifecycle:**
`RegisterConsumer(int hWnd, string szProductName, string
szApplicationName, string szVersion, bool bRetainHiddenAlarms) → int`,
`DeregisterConsumer() → int`,
`InitializeConsumer(string szApplicationName) → int`,
`UninitializeConsumer() → int`,
`Dispose()`.
- **Subscription:**
`Subscribe(string szSubscription, short wFromPri, short wToPri,
eQueryType QueryType, eSortFlags SortFlags, eAlarmFilterState
FilterMask, eAlarmFilterState FilterSpecification) → int`.
- **Change enumeration (pull on poke):**
`GetStatistics(out int lPercentQuery, out int lTotalAlarms, out int
lActiveAlarms, out int lSuppressedAlarms, out int lSuppressedFilters,
out int lNewAlarms, out int lChangesCount, out int[] ChangeCodes,
out int[] ChangePos, out int[] hAlarm) → int`.
- **Record fetch:**
`GetAlarmExtendedRec(int lIndex, out AlarmRecord almRec) → int`,
`GetAlarmExtendedRec2(...)`,
`GetHighPriAlarm(out AlarmRecord almRec) → int`.
- **Selection model** (used by ack-selected-* family):
`DeselectAll`, `SelectAlaramEntry(short select, int from, int to)`,
`SelectByGUID(Guid)`, `SelectAlarmCount(int from, int to)`.
- **Acknowledge:**
`AlarmAckByGUID(Guid alarmGuid, string ackComment, string ackOprName,
string ackOprNode, string ackOprDomain, string ackOprFullName) → int`
is the per-alarm full-fidelity native ack.
`AlarmAckSelected(string ackComment, string ackOprName, string
ackOprNode, string ackOprDomain, string ackOprFullName) → int`
acks whatever the selection model currently has selected.
Several `AckSelected*Group/Tag/Priority/All/Visible*Alarms_Ex(...)`
variants exist for bulk ack scoped to a group / tag / priority range.
- **Suppress / shelve:** `SupressSelected*` and `ShelveSelected*`
families plus `DoAlarmShelveAction(...)`. Out of scope for the v1
alarm path.
- **Snapshot/filter** (`SF*` prefix): `SFSetSortA / SFSetFilterA /
SFCreateSnapshot / SFGetListCount / SFDeleteSnapshot / SFRefreshAlarm /
SFGetStatistics`. Snapshot-style query API, distinct from the
consumer-subscription path. Not currently used.
## What this means
The architecture comment on
`src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs` (PR A.5) is
**wrong against this deployed assembly**:
> "The AVEVA alarm-manager surface (`IAlarmMgrDataProvider`) exposes
> the events we need as plain .NET events — no Windows message pump
> required."
There is no managed event surface. `AlarmClient.RegisterConsumer`
takes an `hWnd` because **WM_APP messaging is the actual notification
mechanism**: AVEVA's alarm provider WM_APP-pokes the registered window,
and the consumer is expected to call `GetStatistics` on each poke to
pull `ChangeCodes` / `ChangePos` / `hAlarm` arrays, then
`GetAlarmExtendedRec(pos, …)` per index to fetch each changed record.
`AlarmClientConsumer.AlarmRecordReceived` has no production callers as
a result — `RaiseAlarmRecordReceived` is `internal` for tests and
never gets invoked at runtime. Until A.2 lands a WM_APP pump,
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` cannot carry events.
## Live runtime probe — 2026-05-01
`MxGateway.Worker.Tests.AlarmClientWmProbeTests.ProbeAlarmClientWmMessages`
is a Skip-gated runtime probe that creates a real message-only
window, calls `AlarmClient.RegisterConsumer(hWnd, …)` +
`Subscribe(@"\Galaxy!", …)`, and pumps for 20s while logging every
window message that arrives. Run results below — this turned the
"WM_APP pump" design assumption upside down.
**`RegisterConsumer` and `Subscribe` both returned 0 (success).** The
calls are valid against the deployed assembly; no parameter pinning
needed.
**A registered-message-class WM (ID `0xC275` in this OS session)
fired every ~1s after `Subscribe` completed.** Constant
`wParam = 0x00001100`, constant `lParam = 0x079E46D8` (looks like a
stable pointer into AVEVA-internal state) for all 20 hits. The
constant payload across hits with no Galaxy alarm being fired
suggests this is a **heartbeat/keepalive**, not a per-change
notification.
**Critically: this WM is delivered to AVEVA's own internal window
(`hwnd=0x18032E`) — NOT to the consumer's `hWnd` we passed in.** The
consumer window's `WndProc` received only the standard creation
sequence (`WM_GETMINMAXINFO`, `WM_NCCREATE`, `WM_NCCALCSIZE`,
`WM_CREATE`) and the destruction sequence (`WM_NCDESTROY`,
`WM_DESTROY`, `WM_NCCALCSIZE`) — nothing in between. AVEVA's
notification path runs entirely against AVEVA's internal window;
it never forwards to the user-supplied hWnd.
The message ID itself is dynamic (a `RegisterWindowMessage`
allocation in the >= 0xC000 range), so it cannot be hard-coded —
each consumer process must call `RegisterWindowMessage` with the
correct *string* and use whatever ID the OS returns.
## What this means for A.2
The "WM_APP pump on the user hWnd" design — what the original plan
banner described and what the previous version of this doc
recommended — does not match how AVEVA actually delivers
notifications. The hWnd parameter to `RegisterConsumer` does not
appear to receive any of AVEVA's alarm traffic; it's likely used
only as a registration identity (and perhaps as a parent for modal
dialogs).
Two viable A.2 designs given the probe data:
1. **Polling.** Just call `GetStatistics` on a timer (e.g. every
500ms in the worker's STA) and react to the change set it
reports. No window plumbing needed. Trade-off: latency floor =
poll period; modest CPU floor because the call is cheap. Matches
the heartbeat-style WM 0xC275 semantics — AVEVA itself runs a
poll loop internally.
2. **Hook AVEVA's internal window.** Discover AVEVA's own window
(`hwnd=0x18032E` in the probe), `SetWindowsHookEx` or
`SetWindowSubclass` on it, and intercept WM 0xC275 on AVEVA's
thread. Higher fidelity, near-zero latency, but invasive,
fragile across AVEVA upgrades, and requires running on the same
process / thread as the AVEVA window. Probably a non-starter
without further AVEVA documentation.
**Recommendation:** the polling path (option 1) is cheaper to
implement, more robust against AVEVA-internal change, and
acceptable for a typical alarm cadence. The worker's existing STA
already provides a thread-affinitized timer surface. The unanswered
question is whether `GetStatistics` can be safely called outside
AVEVA's own message-pump thread — confirmable by extending the
probe to fire `GetStatistics` on its own thread and check the
result.
## Alarm-provider visibility — third probe run, 2026-05-01
Extended the probe to call `AlarmClient.GetProviders` after
`RegisterConsumer`. Result on this rig:
```
GetProviders -> rc=0 count=0 list=[]
```
**Zero alarm providers visible to the consumer process.** This
explains every preceding probe run: no providers means no alarm
events, regardless of how many times any value (including a
bool with an `$Alarm` extension) flips. `Subscribe(@"\Galaxy!")`
returns 0 (success) but matches nothing because the alarm-manager
chain that provides the matching feed doesn't expose any provider
to this consumer.
A System Platform script flipping `TestMachine_001.TestAlarm001`
every 10s during this probe run produced no observable
`GetStatistics` transitions, no `positions[]` / `handles[]`
entries, no change in any field — confirms the silence is not
about subscription-scope / message-pump but about provider
absence.
### Possible causes
1. **No `$Alarm` extension on the test bool.** If
`TestMachine_001.TestAlarm001` is a regular UDA without a
`BoolAlarm` extension wired to it, flipping the value just
writes a new value — no alarm fires.
2. **Alarm manager service not running.** AVEVA's `aaAlarmMgr`
(or the equivalent on this rig's Platform version) needs to
be running for providers to register.
3. **Process security context.** A consumer running under a
normal user account may not see providers that registered
under `LocalSystem` / a Platform service identity. The
gateway-worker installation runs under a service account
that may have access where `dotnet test` doesn't.
## InitializeConsumer required — fourth probe run, 2026-05-01
Adding `InitializeConsumer("AlarmProbe.Tests")` before
`RegisterConsumer` made `\Galaxy!` appear in `GetProviders`
(count=1, status 0 → 100 within 500ms). So #2 and #3 above are
NOT the cause — the consumer can see the alarm provider once it
calls Initialize. That's a missing API-call ordering, not a
permission or service issue.
```
InitializeConsumer -> 0
RegisterConsumer -> 0
GetProviders [after Register] -> rc=0 count=0 list=[]
Subscribe('\Galaxy!') -> 0
GetProviders [after Subscribe] -> rc=0 count=1 list=[ 0 \Galaxy!]
GetProviders [poll #1] -> rc=0 count=1 list=[100 \Galaxy!]
```
Despite the provider being visible at "100% query complete" for
the entire 60s window, `GetStatistics` continued to report
`total=0 active=0 codes=[7]` — no alarm transitions reached the
consumer even with a System Platform script flipping the test
boolean every 10s during the run.
That isolates the remaining unknown to whether the test bool's
alarm extension is actually generating MxAccess alarm-provider
events when its value flips. The probe has confirmed every link
in the consumer chain works (Initialize → Register → Subscribe →
provider visible at 100%) — what's missing is alarm traffic from
the producer side. ObjectViewer or another live consumer running
alongside the script is the next discriminator: does it visibly
see the alarm fire?
API-ordering finding: `InitializeConsumer` MUST precede
`RegisterConsumer` (or at least, must be called before
`GetProviders` returns anything). PR A.5's `AlarmClientConsumer`
omits `InitializeConsumer` entirely — that's a bug fix to apply
even before A.2 lands, since without it the provider chain never
becomes visible.
## Subscribe-parameter sweep — fifth probe run, 2026-05-01
Even with `InitializeConsumer` + provider visible at status 100,
no alarm transitions arrived during a 60s window with the user's
script flipping the test bool every 10s. Tried:
- `qtSummary` and `qtHistory` (the only `eQueryType` values).
- Priority 1..999 and 0..32767.
- `eAlarmFilterState.asNone` and `asAlarmActiveNow` for both
`FilterMask` and `FilterSpecification`.
`eAlarmFilterState` is single-state-valued (asNone=0,
asAlarmActiveNow=1, asAlarmAcked=2, asShelved=3), not flag bits.
None of these knobs surfaced any alarm activity.
User confirmation 2026-05-01: the test bool does have a
`BoolAlarm` extension on it; in `aaObjectViewer` the
`$Alarm.InAlarm` sub-attribute flips true/false in lockstep with
the script's writes. So the alarm extension is **evaluating**
its condition, just not visibly producing transitions on the
`aaAlarmManagedClient` consumer stream.
## Multi-channel + multi-subscription probe — sixth run, 2026-05-01
Extended the probe to try every consumer-side approach in
parallel:
- **Subscription expressions** (sequential): `\Galaxy!`,
`\Galaxy!*`, `\\Galaxy!`, `\Galaxy!TestArea`, `\\.\Galaxy!`.
All Subscribe calls returned rc=0; the last one
(`\\.\Galaxy!`) is reflected in `GetProviders` (count=1).
- **Read channels** polled at 500ms cadence: `GetStatistics`,
`GetHighPriAlarm`, `SFCreateSnapshot` + `SFGetStatistics`.
- **Filter+sort**: priority 0..32767, `qtSummary`,
state=`asAlarmActiveNow`, sort=`sfReturnNewestFirst`.
- **AlarmRecord init** (worked around `Not a valid Win32
FileTime` exception): all DateTime fields pre-set to FILETIME
epoch (1601-01-01 UTC) before the call, since
`default(DateTime)` is outside FILETIME range and trips the
interop marshaler.
Result of the 60s run with `TestMachine_001.TestAlarm001` being
flipped every 10s:
```
Subscribe('\Galaxy!') -> 0
Subscribe('\Galaxy!*') -> 0
Subscribe('\\Galaxy!') -> 0
Subscribe('\Galaxy!TestArea') -> 0
Subscribe('\\.\Galaxy!') -> 0
GetProviders [after Subscribe-multi] -> count=1 list=[ 0 \\.\Galaxy!]
GetStatistics #1: total=0 active=0 changes=1 codes=[7] positions=[] handles=[]
GetHighPriAlarm #1: rc=0 { }
SF channel #1: SFCreate=0 numAlarms=0 SFStats=0 unackRet=0 unackAlm=0 ackAlm=0 others=0 events=0 idxNewest=-1
```
**No further "(changed)" entries for the entire 60s window.**
Every read API returned the same empty result on every poll.
User confirms the alarm IS firing — `aaObjectViewer` sees
`$Alarm.InAlarm` flip in lockstep with the script. Historian
records exist (per user — needs verification by querying the
historian directly).
## Conclusion of consumer-side probing
`aaAlarmManagedClient.AlarmClient` is **not** the receive
surface AVEVA's alarm pipeline routes to in this Galaxy
configuration. The consumer chain is verified end-to-end:
- `InitializeConsumer` + `RegisterConsumer` + `Subscribe` all
succeed (rc=0).
- `GetProviders` finds `\Galaxy!` once Initialize is called.
- All read APIs (`GetStatistics`, `GetHighPriAlarm`,
`SFCreateSnapshot`/`SFGetStatistics`) return empty even with
every documented filter combination.
- The consumer's hWnd receives zero AVEVA messages between
`WM_CREATE` and `WM_DESTROY`; AVEVA's traffic goes to its own
internal hwnd.
The next investigation directions are not consumer-side:
1. **Inspect `aaObjectViewer`'s alarm SDK** to see what library
it uses to read alarms. If different from
`aaAlarmManagedClient`, switch the worker over.
2. **Query the historian directly** (`aahEventStorage` /
`aahEventSvc`) to confirm alarms are recorded — and use the
same path for v2 alarm capture.
3. **Inspect AVEVA's alarm-routing config** for this Galaxy in
System Platform IDE — area assignments, alarm provider
bindings, "publish alarm events to" settings on the platform.
For A.2 implementation: the `aaAlarmManagedClient` path the
gateway-worker is currently architected around may be a
dead-end on customer Galaxies configured this way. If the
alarms truly only flow through the historian event-storage path,
A.2 needs to consume from `aahEventStorage` instead — a
fundamental architecture pivot.
## BREAKTHROUGH — seventh probe run, 2026-05-01
Two changes finally produced a signal:
1. **Subscription scope:** `\\<MachineName>\Galaxy!<TopArea>` is the
canonical AlarmClient subscription format (per ArchestrA Alarm
Client docs at `archestra6.rssing.com/chan-12008125/article13.html`):
`\\Node\Provider!Area!Filter`, where Node is the *machine* name,
Provider is **literally `Galaxy`**, and Area is a hosted area
object. For this rig (`\\DESKTOP-6JL3KKO\Galaxy!DEV`) the DEV
area — the platform's primary area — is the right scope. Earlier
`\Galaxy!`, `\Galaxy!TestArea`, `\\.\Galaxy!`, etc., all returned
rc=0 but matched no traffic — they were not the canonical form.
2. **`InitializeConsumer` before `RegisterConsumer`** — already
discovered earlier; bug-fix for PR A.5's `AlarmClientConsumer`.
With both in place, `GetHighPriAlarm` returned a record on every
poll for 60s straight (117/117 calls), but threw
`ArgumentOutOfRangeException: Not a valid Win32 FileTime` instead
of returning successfully — the AlarmRecord struct contains five
DateTime fields (`ar_Time`, `ar_OrigTime`, `ar_AckTime`,
`ar_RtnTime`, `ar_SubTime`) and AVEVA writes sentinel/invalid
FILETIME values for unset ones (e.g., `ar_AckTime` for an
unacknowledged alarm). The .NET interop that AVEVA ships
(`aaAlarmManagedClient.dll`) auto-converts FILETIME→DateTime and
rejects out-of-range values.
`GetStatistics` continues to report `total=0 active=0` even with
GetHighPriAlarm returning records — those two API surfaces have
genuinely different views in AVEVA's data model.
So: **alarms flow through `aaAlarmManagedClient.AlarmClient` once
the subscription expression is canonical**. The blocking issue is
extracting the payload past the .NET interop's DateTime
auto-marshaling.
## Remaining work to capture alarm payloads
Define a custom COM interop that uses `long` (FILETIME-as-int64)
instead of `DateTime` for the timestamp fields. Approach options:
1. **Patch the AVEVA-shipped `aaAlarmManagedClient.dll`** — ildasm
the assembly, replace `DateTime` with `long` on AlarmRecord's
timestamp fields, ilasm back. Brittle across AVEVA upgrades.
2. **Write our own `[ComImport]` interface** — declare
`IRawAlarmConsumer` ourselves with safe-blittable types,
discover the underlying COM IID (via reflection on
`AlarmClient`'s `[Guid]` attribute), and `(IRawAlarmConsumer)
alarmClient` cast. Cleaner; requires the IID.
3. **Use `IDispatch` late binding** — dispatch-Invoke bypasses
strong-typed marshaling. Verbose but doesn't need IIDs.
For PR A.2's worker integration, option 2 is the least
disruptive. Once the interop is custom, `AlarmClient.Subscribe` +
`GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable
polling-style alarm consumer.
**REVISED 2026-05-01 — option 1 not directly applicable.**
Reflection on `aaAlarmManagedClient.AlarmClient` shows it
implements only `IDisposable` (no `[ComImport]` interface, no
class GUID). It has a single field `CwwAlarmConsumer*
m_almUnmanaged` — meaning `AlarmClient` is a **C++/CLI managed
wrapper around a native C++ class**, NOT a COM-interop class.
The DateTime conversion happens inside the AVEVA wrapper's IL,
not at a .NET-to-COM marshaling boundary. There is no separate
COM interface IID we can QI to.
Revised approach options:
A. **Switch to `wnwrapConsumer.dll`** — a separate standalone
COM library AVEVA ships at
`C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll`
exposing `WNWRAPCONSUMERLib.wwAlarmConsumerClass` with
`SetXmlAlarmQuery` / `GetXmlCurrentAlarms`. XML-string output
bypasses FILETIME marshaling entirely.
B. **Patch `aaAlarmManagedClient.dll` IL** — wrap the unsafe
`DateTime.FromFileTime` calls with a safe variant. Direct
fix but modifies a vendor binary.
C. **Reflect into `m_almUnmanaged` and call native vtable**
get the IntPtr, walk the MSVC C++ vtable, call
`__thiscall` methods via `Marshal.GetDelegateForFunctionPointer`.
Doable but requires reverse-engineering the C++ class layout.
Option A is the best fit: real COM-based, self-contained in
our code, conventional production-grade approach (the WIN-911
consumer pattern referenced in AVEVA support forums uses it).
The polling-vs-WM_APP-callback question from earlier is now
moot: `GetStatistics`'s `positions[]/handles[]` arrays remained
empty even when alarms were demonstrably present. The active
read API for current alarms is `GetHighPriAlarm`, not
`GetStatistics`'s change array.
### Implications for A.2 implementation
The A.2 PR's value is unmeasurable until at least one alarm
provider is visible. The choice between polling-via-`GetStatistics`
and the callback path can only be decided by observing what
populates first when a real alarm fires. Without a provider,
both paths return the same "nothing happening" answer.
Until that's resolved, A.2 implementation work is genuinely
blocked on a dev-rig configuration issue — not on architectural
choice or code structure.
## GetStatistics polling — second probe run, 2026-05-01
Extended the probe to call `GetStatistics` every ~2s alongside the
WM logger. Key findings:
- **`GetStatistics` is safely callable from the same thread that
did `RegisterConsumer` + `Subscribe`.** Every poll returned rc=0
with no exceptions over 9 polls / 20s window.
- **The deployed Galaxy currently has zero active alarms.** Every
poll reported `total=0 active=0 suppressed=0 newAlarms=0`. The
`positions[]` and `handles[]` arrays were empty.
- **`changes=1 codes=[7]` was constant across all polls**, matching
the constant 1 Hz WM 0xC275 cadence. Code 7 is consistent with a
"heartbeat / subscription healthy" sentinel — same semantics as
the WM but reported through the pull-side API.
- `percent=100` (query-complete percentage) was constant — the
subscription is steady-state.
This confirms the polling design (option 1 in the previous section)
is mechanically viable. The remaining open question is whether
`GetStatistics` populates `positions[] / handles[]` with real
entries when an alarm transition actually fires — proving that
requires firing an alarm.
## Open follow-up probes
Each can be added to `AlarmClientWmProbeTests` as a separate
Skip-gated test:
1. **Fire a real Galaxy alarm during the pump window.** The cleanest
programmatic trigger is an MxAccess write that flips a
`$Alarm`-extended boolean to true (alarm in) and back to false
(alarm out). Pinning the exact tag reference is pending — needs
either a documented test-fixture tag or an interactive selection
in System Platform IDE. Once the trigger fires, this resolves
whether AVEVA's pulled change set arrives via `GetStatistics`
`positions[] / handles[]` (per-change polling works) or only via
the AVEVA-internal window (callback path needed).
2. **Hook AVEVA's internal window** to log what WMs it actually
processes — only relevant if probe 1 shows `GetStatistics` does
NOT report per-change activity.
3. **Decompile `aaAlarmManagedClient.dll`'s IL** for the
`RegisterConsumer` method to find what `RegisterWindowMessage`
string is used and whether there's a callback-registration
surface on `WNAL_Register` that the managed client wraps. The
alarmlst.dll strings (`WNAL_CallBack`, "Invalid callbacks" error)
suggest the underlying C API takes callbacks, but the managed
wrapper exposes none of them.
PR A.5's `Subscribe` / `AcknowledgeByGuid` / `SnapshotActiveAlarms`
are correct — they're pull-style and don't depend on the
notification mechanism.
## Option A — captured, 2026-05-01
`wnwrapConsumer.dll` (`C:\Program Files (x86)\Common Files\
ArchestrA\wnwrapConsumer.dll`) hosts the standalone COM class
`WNWRAPCONSUMERLib.wwAlarmConsumerClass`. Type library imports
cleanly via `tlbimp` (output stored under `mxaccessgw/lib/
Interop.WNWRAPCONSUMERLib.dll`). The COM class is registered in
`HKLM:\SOFTWARE\WOW6432Node\Classes\CLSID\
{7AB52E5F-36B2-4A30-AE46-952A746F667C}` with `ThreadingModel=
Apartment` — `new wwAlarmConsumerClass()` succeeds via
`CoCreateInstance`.
The probe `MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs`
(Skip-gated, archival) drove the captured run. Lifecycle:
1. `new wwAlarmConsumerClass()` — instantiated.
2. `InitializeConsumer("MxGatewayProbe.WnWrap")` -> 0.
3. `RegisterConsumer(hWnd: 0, productName, applicationName,
version)` -> 0. **Note:** wnwrap's `RegisterConsumer` is
4-arg (no `bRetainHiddenAlarms`); `aaAlarmManagedClient`'s
is 5-arg. Different surface.
4. `Subscribe(@"\\<machine>\Galaxy!DEV", priLow=1, priHigh=999,
qtSummary, sfReturnNewestFirst, asAlarmActiveNow,
asAlarmActiveNow)` -> 0. Same canonical scope that worked
for `aaAlarmManagedClient`.
5. `SetXmlAlarmQuery(...)` was called too but the round-trip
`GetXmlAlarmQuery` returned a mangled echo (NODE became
`DESKTOP-6JL3KKO\Galaxy!DEV`, PROVIDER became `Galaxy!DEV`,
ALARM_STATE shortened to `All`, DISPLAY_MODE truncated to
`Sum`). The XML-query path looks broken in this build; rely
on `Subscribe` for the filter and skip `SetXmlAlarmQuery` in
production. Confirming "Subscribe alone is sufficient" is
one follow-up probe (call `Subscribe` and read XML, no
`SetXmlAlarmQuery`) — out of scope for the breakthrough run
but easy to verify.
### Captured XML (60 polls over 30s, 500ms cadence)
`GetXmlCurrentAlarms2(maxAlmCnt: 100, out vartCurrentXmlAlarms)`
returned BSTR XML cleanly on every call — 60/60 ok, zero throws.
`GetXmlCurrentAlarms` (the v1 method) returned identical content
on the same cadence; either method is viable.
Empty state:
```xml
<?xml version="1.0"?><ALARM_RECORDS COUNT="0"></ALARM_RECORDS>
```
With alarm active (`UNACK_ALM`, value=true after the flip
script set the bool true):
```xml
<?xml version="1.0"?>
<ALARM_RECORDS COUNT="1">
<ALARM>
<GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>
<DATE>2026/5/1</DATE>
<TIME>13:26:14.709</TIME>
<GMTOFFSET>240</GMTOFFSET>
<DSTADJUST>0</DSTADJUST>
<PROVIDER_NODE>DESKTOP-6JL3KKO</PROVIDER_NODE>
<PROVIDER_NAME>Galaxy</PROVIDER_NAME>
<GROUP>TestArea</GROUP>
<TAGNAME>TestMachine_001.TestAlarm001</TAGNAME>
<TYPE>DSC</TYPE>
<VALUE>true</VALUE>
<LIMIT>true</LIMIT>
<PRIORITY>500</PRIORITY>
<STATE>UNACK_ALM</STATE>
<OPERATOR_NODE></OPERATOR_NODE>
<OPERATOR_NAME></OPERATOR_NAME>
<ALARM_COMMENT>Test alarm #1</ALARM_COMMENT>
</ALARM>
</ALARM_RECORDS>
```
After the script set the bool false (`UNACK_RTN`, value=false):
```xml
<?xml version="1.0"?>
<ALARM_RECORDS COUNT="1">
<ALARM>
<GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>
<DATE>2026/5/1</DATE>
<TIME>13:26:24.710</TIME>
...
<VALUE>false</VALUE>
<STATE>UNACK_RTN</STATE>
...
</ALARM>
</ALARM_RECORDS>
```
The 10s cadence between transitions matches the System Platform
script's flip frequency exactly. **GUID is stable across the
in→out cycle** (`BCC4705…` carried through both states), so the
XML stream represents the alarm record's lifecycle, not separate
event records — this is "current alarms snapshot," not
"transition stream." For an OPC UA `AlarmConditionService`
adapter this is fine: condition-state changes per-snapshot is
the supported model.
`STATE` enum values observed: `UNACK_RTN` (the alarm has
returned to normal but is unacknowledged — i.e., visible in the
"current alarms" list because operator hasn't acked it yet) and
`UNACK_ALM` (the alarm is currently active and unacknowledged).
The other states from `eAlmState` (`ACK_RTN`, `ACK_ALM`) would
appear when an ack is performed — `wwAlarmConsumerClass.AlarmAckByGUID`
is the method to call.
### `GetStatistics` AV — unrelated quirk
Every `GetStatistics` call threw `AccessViolationException` in
the probe. Cause: the wnwrap interop signature uses `IntPtr` for
the three array out-parameters (`pChangeCode`, `pChangePos`,
`phAlarm`); passing `IntPtr.Zero` is wrong — the COM impl is
writing into the buffer pointer without null-checking. Pre-
allocate three int-arrays and pass pinned pointers (or use
`Marshal.AllocCoTaskMem`) to fix. Not required for the
production path — the XML methods give us everything we need.
### Implications for PR A.2 worker integration
Replacing `aaAlarmManagedClient.AlarmClient` with
`WNWRAPCONSUMERLib.wwAlarmConsumerClass` in the worker's
alarm-consumer surface unblocks A.2 fully. Outline:
1. **Reference path:** drop `aaAlarmManagedClient.dll` reference
from `MxGateway.Worker.csproj`; add `Interop.WNWRAPCONSUMERLib.dll`
reference from `mxaccessgw/lib/`. (Or commit the interop dll
in-tree under `lib/` and reference relatively.)
2. **`AlarmClientConsumer``WnWrapAlarmConsumer`:** rewrite
the consumer wrapper to:
- `new wwAlarmConsumerClass()` on the worker's STA thread.
- `InitializeConsumer(applicationName)` then
`RegisterConsumer(hWnd: 0, …)`.
- `Subscribe(@"\\<node>\Galaxy!<area>", …)` per configured
area. The `<node>` and `<area>` are configurable (default
`Environment.MachineName` + the platform's primary area).
- Poll `GetXmlCurrentAlarms2(maxAlmCnt, out xml)` on a
timer (500ms-1s cadence is comfortable). Parse XML
payload; diff against the previous snapshot (keyed by
`GUID`); emit `MX_EVENT_FAMILY_ON_ALARM_TRANSITION`
events for added/changed/removed records.
- `AlarmAckByGUID(VBGUID, comment, oprName, node, domain,
fullName)` for client-driven acknowledgements (matches
PR A.5's `AlarmAckCommand` payload).
- Lifecycle teardown: `DeregisterConsumer` +
`UninitializeConsumer` + `Marshal.FinalReleaseComObject`.
3. **Conversion layer:** map XML record fields to
`MxAlarmConditionRecord` proto:
- `GUID``condition_id` (canonicalize the no-dashes hex
to a UUID string).
- `STATE` enum → `inAlarm` + `acked` booleans
(`UNACK_ALM` → in_alarm=true, acked=false;
`UNACK_RTN` → in_alarm=false, acked=false;
`ACK_ALM` → in_alarm=true, acked=true;
`ACK_RTN` → in_alarm=false, acked=true).
- `DATE + TIME + GMTOFFSET + DSTADJUST` → reassemble UTC
timestamp; matches the worker's existing `Timestamp`
wire format.
- `PRIORITY` → severity (already 1-1000-ish range).
- `TAGNAME` → reference; `PROVIDER_NAME` + `GROUP` for
scope metadata.
4. **PR A.5 fix carry-over:** `InitializeConsumer` MUST be
called before `RegisterConsumer` (rediscovered with
`aaAlarmManagedClient`, also true here). The existing
`AlarmClientConsumer` skips Initialize entirely; the new
`WnWrapAlarmConsumer` includes it from day one.
5. **Test reuse:** PR A.5's snapshot/ack contract tests can
stay — they don't touch the underlying COM API. Add a new
integration test against the wnwrap surface (live-AVEVA-only,
Skip-gated like the probe).
### Settled API-ordering and surface knowledge
- `InitializeConsumer` first, then `RegisterConsumer` — both
on `aaAlarmManagedClient.AlarmClient` and
`wwAlarmConsumerClass`.
- `RegisterConsumer` arity differs:
`aaAlarmManagedClient.AlarmClient.RegisterConsumer(hWnd,
product, app, version, bRetainHiddenAlarms)` — 5 args;
`wwAlarmConsumerClass.RegisterConsumer(hWnd, product, app,
version)` — 4 args. The wnwrap class has no
`bRetainHiddenAlarms` parameter at all.
- Subscription expression format: `\\<machine>\Galaxy!<area>`
(literal `Galaxy` provider) for both libraries.
- Native ack: `AlarmAckByGUID(VBGUID guid, comment, oprName,
node, domain, fullName)` on the v2 surface; ID 5-arg
variant on the legacy `IwwAlarmConsumer` interface.
These findings retire the open follow-up probes from the
"polling-vs-pump" debate above — `wwAlarmConsumerClass` plus
poll-on-timer is the implementation.
## Live smoke-test discoveries — 2026-05-01
The Skip-gated `AlarmsLiveSmokeTests.Alarms_full_pipeline_round_trip`
ran the full
`WnWrapAlarmConsumer` + `AlarmDispatcher` + `MxAccessAlarmEventSink`
pipeline against the dev rig with the flip script running. End-to-end
verified: 6 real transitions captured on the 10s cadence, ack-by-name
returned rc=0, pipeline stayed healthy through 5 more transitions
afterwards. Three production-relevant quirks surfaced and were fixed
in the consumer:
### 1. `SetXmlAlarmQuery` is mandatory for reads despite the mangled echo
Without `SetXmlAlarmQuery`, the first `GetXmlCurrentAlarms2` call
fails with `E_FAIL` (HRESULT `0x80004005`). The discovery doc above
flagged the round-trip echo as mangled and recommended skipping the
call — that recommendation is **wrong**. The echo *is* mangled (AVEVA
parses NODE/PROVIDER/ALARM_STATE/DISPLAY_MODE incorrectly), but the
call itself is required as some kind of subscription enabler. Even
the Subscribe call setting the actual filter doesn't avoid the need
for `SetXmlAlarmQuery`.
`WnWrapAlarmConsumer.ComposeXmlAlarmQuery(subscription)` decomposes
the canonical `\\<machine>\Galaxy!<area>` form into the XML's
NODE/PROVIDER/GROUP fields. Mangled or not, the call enables reads.
### 2. Two consumers required: read-side vs. ack-side
`SetXmlAlarmQuery` enables reads but **breaks `AlarmAckByName` on
the same consumer instance**. With SetXml applied, AlarmAckByName
returns -55 even with valid name+provider+group+operator. Without
SetXml, AlarmAckByName succeeds with rc=0.
The production consumer therefore provisions **two** wnwrap COM
instances:
- Primary consumer (`client`): runs full lifecycle including
`SetXmlAlarmQuery` for `GetXmlCurrentAlarms2` polls.
- Ack-only consumer (`ackClient`): runs Initialize → Register →
Subscribe via the v1-prefixed methods, **no SetXmlAlarmQuery**.
All `AcknowledgeByName` calls dispatch through this instance.
Both consumers subscribe to the same expression. Disposal cleans up
both via a shared `ReleaseConsumerCom` helper.
### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg
`wwAlarmConsumerClass` exposes two `AlarmAckByName` overloads:
- `IwwAlarmConsumer2` v2: 8 args (`name, provider, group, comment,
oprName, node, domainName, oprFullName`).
- `IwwAlarmConsumer` v1: 6 args (no domain, no full-name).
The v2 8-arg method returns -55 on this AVEVA build regardless of
operator-identity inputs — looks like a stub. The v1 6-arg method
works. Production `WnWrapAlarmConsumer.AcknowledgeByName` calls the
6-arg overload and discards the proto's `domain` + `full_name` fields.
The proto contract keeps the 8 fields for forward compatibility if
AVEVA fixes the v2 method later.
### 4. `AlarmAckByGUID` is not implemented
The v2 `AlarmAckByGUID(VBGUID, …)` throws `NotImplementedException`
(COM `E_NOTIMPL`) on `wwAlarmConsumerClass` against this AVEVA
build. The reference→GUID lookup that we initially planned to wire
through `AlarmAckByGUID` is therefore not viable on wnwrap; all acks
must go through `AlarmAckByName`.
The proto `AcknowledgeAlarmCommand` (GUID-based) and the worker's
`MxAccessCommandExecutor.ExecuteAcknowledgeAlarm` switch arm remain
in the codebase for the forward-compat shape, but the gateway-side
`WorkerAlarmRpcDispatcher.AcknowledgeAsync` now always routes through
`AcknowledgeAlarmByName` when the public RPC supplies a recognizable
`Provider!Group.Tag` reference.
**Command/reply payload reuse.** `MxCommand.payload` has a dedicated
`acknowledge_alarm_by_name_command` field, but `MxCommandReply.payload`
intentionally has **no** by-name-specific case. The by-name ack carries
no outcome detail beyond the native return code, so the worker's
`ExecuteAcknowledgeAlarmByName` sets the same `acknowledge_alarm`
(`AcknowledgeAlarmReplyPayload`) reply case used by the GUID arm, with
`native_status` = the `AlarmAckByName` return code (also echoed into the
top-level `MxCommandReply.hresult`). Reply consumers must dispatch on
`MxCommandReply.kind` (`MX_COMMAND_KIND_ACKNOWLEDGE_ALARM` vs.
`MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME`), not on the payload oneof
case, to distinguish the two acks. `WorkerAlarmRpcDispatcher` reads only
the top-level `hresult`/`protocol_status`, so it handles both arms
without unpacking the payload.
**Worker `native_status` → public `AcknowledgeAlarmReply` mapping.** The
worker carries the ack outcome as a single `int32`
(`AcknowledgeAlarmReplyPayload.native_status`, the `AlarmAckByName` /
`AlarmAckByGUID` return code; `0` = success), also mirrored into the
worker `MxCommandReply.hresult`. The public `AcknowledgeAlarmReply` has
two outcome-shaped fields, but only one is populated:
- `AcknowledgeAlarmReply.hresult``WorkerAlarmRpcDispatcher` copies the
worker's `MxCommandReply.hresult` (the native return code) into this
field. **This is the authoritative ack-outcome field**; `0` means the
ack succeeded. It is absent only when the worker reply omitted the
value, which is a protocol violation surfaced in `protocol_status`.
- `AcknowledgeAlarmReply.status` (`MxStatusProxy`) — the worker by-name /
by-GUID ack path produces only the `int32` return code, never a
populated `MXSTATUS_PROXY` struct, so `WorkerAlarmRpcDispatcher` leaves
this field **unset on every reply**. It is reserved for a future
structured view of the ack outcome. Clients must not depend on it.
Client authors should therefore branch on `protocol_status` first (for
transport/session-level failures) and then on `hresult` (`0` = ack
accepted by MXAccess) — never on `status`.
### 5. STA / threading — production fix needed
The wnwrap COM is `ThreadingModel=Apartment`. The consumer's
internal `Timer` fires on threadpool threads and would block forever
on cross-apartment marshaling unless the host STA pumps Win32
messages. The smoke test sidesteps this by setting
`pollIntervalMilliseconds=0` (Timer disabled) and driving `PollOnce`
manually from the test's STA. Production hosting will route polls
through the worker's `StaRuntime` in a follow-up — the consumer's
`PollOnce` is `public` and idempotent so the wire-up is mechanical.
### Capture summary
```
Transition: kind=Clear ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' …
Transition: kind=Raise ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' …
SnapshotActiveAlarms count=1
active: ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' state=Active
AcknowledgeByName(real identity) -> rc=0
Post-ack transition: kind=Clear …
+1: kind=Raise … (10s after ack)
+2: kind=Clear … (20s)
+3: kind=Raise … (30s)
+4: kind=Clear … (40s)
```
10s cadence held throughout; full proto fields populated correctly;
ack registered server-side without errors.
+3 -3
View File
@@ -103,7 +103,7 @@ public string ResolveRequiredScope(object request)
StreamEventsRequest => GatewayScopes.EventsRead,
MxCommandRequest commandRequest => ResolveCommandScope(commandRequest.Command?.Kind ?? MxCommandKind.Unspecified),
AcknowledgeAlarmRequest => GatewayScopes.InvokeWrite,
QueryActiveAlarmsRequest => GatewayScopes.EventsRead,
StreamAlarmsRequest => GatewayScopes.EventsRead,
TestConnectionRequest or
GetLastDeployTimeRequest or
DiscoverHierarchyRequest or
@@ -113,7 +113,7 @@ public string ResolveRequiredScope(object request)
}
```
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. `AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and `QueryActiveAlarms` shares the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so it carries `events:read`.
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. `AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and `StreamAlarms` shares the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so it carries `events:read`. Both alarm RPCs are session-less: the scope check is the only authorization gate, since there is no per-session ownership to enforce.
`MxCommandRequest` is special because it multiplexes many MxAccess operations through a single RPC. The resolver inspects the embedded `MxCommandKind` so each operation gets its own scope:
@@ -205,7 +205,7 @@ blocking constraint; secured values and raw credentials are never logged.
|----------|-------|--------------|
| `SessionOpen` | `session:open` | `OpenSessionRequest` |
| `SessionClose` | `session:close` | `CloseSessionRequest` |
| `EventsRead` | `events:read` | `StreamEventsRequest`, `QueryActiveAlarmsRequest`, `MxCommandKind.DrainEvents` |
| `EventsRead` | `events:read` | `StreamEventsRequest`, `StreamAlarmsRequest`, `MxCommandKind.DrainEvents` |
| `InvokeRead` | `invoke:read` | `MxCommandRequest` for read-style command kinds (`Register`, `AddItem`, `Advise`, `ReadBulk`, and any kind not otherwise mapped) |
| `InvokeWrite` | `invoke:write` | `AcknowledgeAlarmRequest`, `MxCommandKind.Write`, `MxCommandKind.Write2`, `MxCommandKind.WriteBulk`, `MxCommandKind.Write2Bulk` |
| `InvokeSecure` | `invoke:secure` | `MxCommandKind.WriteSecured`, `MxCommandKind.WriteSecured2`, `MxCommandKind.WriteSecuredBulk`, `MxCommandKind.WriteSecured2Bulk`, `MxCommandKind.AuthenticateUser` |
+18
View File
@@ -61,6 +61,12 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
"CommandTimeoutSeconds": 60,
"DashboardRefreshIntervalSeconds": 30
},
"Alarms": {
"Enabled": false,
"SubscriptionExpression": "",
"DefaultArea": "",
"ReconcileIntervalSeconds": 30
}
}
}
@@ -168,6 +174,18 @@ at startup.
See [Galaxy Repository Browse](./GalaxyRepository.md) for the RPC surface and
behavior.
## Alarm Options
| Option | Default | Description |
|--------|---------|-------------|
| `MxGateway:Alarms:Enabled` | `false` | Gates the gateway's always-on central alarm monitor. When `true`, the gateway opens one gateway-owned worker session dedicated to alarms, caches the active-alarm set, and fans it out to every client through the `StreamAlarms` RPC — no client opens its own session to see alarms. |
| `MxGateway:Alarms:SubscriptionExpression` | _(empty)_ | AVEVA alarm-subscription expression the monitor subscribes on startup, in canonical `\\<machine>\Galaxy!<area>` form. The literal `Galaxy` provider is correct regardless of the Galaxy database name. When empty and `Enabled` is `true`, the gateway falls back to `\\<MachineName>\Galaxy!<DefaultArea>` if `DefaultArea` is set. |
| `MxGateway:Alarms:DefaultArea` | _(empty)_ | Area name used to compose a default subscription when `SubscriptionExpression` is empty. If both are empty while `Enabled` is `true`, the monitor faults with a configuration diagnostic. |
| `MxGateway:Alarms:ReconcileIntervalSeconds` | `30` | How often the monitor reconciles its in-process alarm cache against the worker's authoritative active-alarm snapshot, catching transitions the live poll-and-diff feed missed. Floored at 5 seconds. |
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
`StreamAlarms` are session-less RPCs served by the monitor.
## Related Documentation
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
+22 -18
View File
@@ -274,28 +274,32 @@ diagnostic session/worker views.
### Alarms page
`/dashboard/alarms` lists the alarms the dashboard session's worker currently
reports as Active or ActiveAcked, refreshed every three seconds. It defaults to
showing unacknowledged `Active` alarms; filters add acknowledged alarms and
narrow by area, severity range, and a reference/source/description text search.
Cleared alarms are not retained — the gateway holds no alarm-history store, so
the page reflects only the live active set. The page is read-only; it does not
acknowledge alarms. If `MxGateway:Alarms:Enabled` is false the session is never
subscribed to an alarm provider, and the page says so instead of showing an
empty list with no explanation.
`/dashboard/alarms` lists the alarms the gateway's central alarm monitor
currently holds as Active or ActiveAcked, refreshed every three seconds. It
defaults to showing unacknowledged `Active` alarms; filters add acknowledged
alarms and narrow by area, severity range, and a reference/source/description
text search. Cleared alarms are not retained — the gateway holds no
alarm-history store, so the page reflects only the live active set. The page is
read-only; it does not acknowledge alarms. If `MxGateway:Alarms:Enabled` is
false the central monitor never starts, and the page says so instead of showing
an empty list with no explanation.
### Live data source
Both the Browse subscription panel and the Alarms page read live MXAccess data
through `IDashboardLiveDataService` (`DashboardLiveDataService`). It owns one
shared gateway session for the whole dashboard, opened lazily on first use via
`ISessionManager` and re-opened transparently when it faults or its lease
expires. One session means one worker process backs every dashboard circuit;
all access is serialised so the worker sees one in-flight command at a time.
Tag reads go through `GatewaySession.SubscribeBulkAsync` / `ReadBulkAsync`;
alarm queries go through `IAlarmRpcDispatcher`. Alarm subscription is the
gateway's existing auto-subscribe-on-open hook, so the dashboard session is
alarm-subscribed only when `MxGateway:Alarms:Enabled` is set.
through `IDashboardLiveDataService` (`DashboardLiveDataService`). For tag data
it owns one shared gateway session for the whole dashboard, opened lazily on
first use via `ISessionManager` and re-opened transparently when it faults or
its lease expires. One session means one worker process backs every dashboard
circuit; all access is serialised so the worker sees one in-flight command at a
time. Tag reads go through `GatewaySession.SubscribeBulkAsync` / `ReadBulkAsync`.
The Alarms page does **not** use the dashboard session: alarm data comes from
the gateway's always-on central monitor. `QueryAlarmsAsync` reads
`IGatewayAlarmService.CurrentAlarms` — the monitor's in-process cache — so the
dashboard sees the same active-alarm set as every `StreamAlarms` client, with
no per-dashboard alarm subscription. When `MxGateway:Alarms:Enabled` is false
the monitor never starts and the cache stays empty.
### API keys page
+6 -6
View File
@@ -29,7 +29,7 @@ A second gRPC service, `GalaxyRepositoryGrpcService`, is mapped alongside it. It
## RPC Handlers
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto` — six in total: `OpenSession`, `CloseSession`, `Invoke`, `StreamEvents`, `AcknowledgeAlarm`, and `QueryActiveAlarms`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto` — six in total: `OpenSession`, `CloseSession`, `Invoke`, `StreamEvents`, `AcknowledgeAlarm`, and `StreamAlarms`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
Public gRPC send and receive message sizes are configured from
`MxGateway:Protocol:MaxGrpcMessageBytes` (default 16 MiB). Official clients use
@@ -88,11 +88,11 @@ Carrying the enqueue timestamp into the worker layer is what lets queue-wait tim
### `AcknowledgeAlarm`
`AcknowledgeAlarm` is a unary RPC that acknowledges a single alarm. The handler validates `session_id` and `alarm_full_reference` inline (it does not run through `MxAccessGrpcRequestValidator`, because the alarm surface routes through `IAlarmRpcDispatcher` rather than the generic `Invoke` path), resolves the session, then delegates to the registered `IAlarmRpcDispatcher`. The production `WorkerAlarmRpcDispatcher` routes the ack over the worker IPC by GUID (`AcknowledgeAlarmCommand`) when the reference parses as a canonical GUID, or by `Provider!Group.Tag` reference (`AcknowledgeAlarmByNameCommand`) otherwise. The handler-level RPC behaviour and the alarm contract itself are documented in [Alarm Client Discovery](./AlarmClientDiscovery.md).
`AcknowledgeAlarm` is a unary, **session-less** RPC that acknowledges a single alarm. The handler validates `alarm_full_reference` inline (it does not run through `MxAccessGrpcRequestValidator`) and delegates to `IGatewayAlarmService.AcknowledgeAsync`. The always-on `GatewayAlarmMonitor` routes the ack over its own gateway-managed worker session — clients no longer open a session to acknowledge an alarm. A reference that parses as a canonical GUID forwards to `AcknowledgeAlarmCommand`; a `Provider!Group.Tag` reference forwards to `AcknowledgeAlarmByNameCommand`.
### `QueryActiveAlarms`
### `StreamAlarms`
`QueryActiveAlarms` is a server-streaming RPC that returns an `ActiveAlarmSnapshot` per currently active alarm. The handler validates `session_id` inline, resolves the session, and delegates to `IAlarmRpcDispatcher`; `WorkerAlarmRpcDispatcher` issues a `QueryActiveAlarmsCommand` over the worker IPC and streams each snapshot from the worker reply.
`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
## Validation Rules
@@ -104,8 +104,8 @@ Carrying the enqueue timestamp into the worker layer is what lets queue-wait tim
| `CloseSession` | `session_id` must be non-empty. | `InvalidArgument` |
| `StreamEvents` | `session_id` must be non-empty. | `InvalidArgument` |
| `Invoke` | `session_id` non-empty, `command` present, `kind` not `Unspecified`, payload oneof must match `kind`. | `InvalidArgument` |
| `AcknowledgeAlarm` | `session_id` and `alarm_full_reference` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
| `QueryActiveAlarms` | `session_id` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
| `AcknowledgeAlarm` | `alarm_full_reference` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
| `StreamAlarms` | No required fields — `alarm_filter_prefix` is optional. | — |
The payload-vs-kind check matters because the `MxCommand.payload` oneof is non-discriminated on the wire — a misaligned client could send `kind = Write` with a `Register` payload and silently confuse the worker. The validator turns that into a clear client error: