From 899ad6e106ebea5dd525090e1ed67ade50d7d2c9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 14:52:03 -0400 Subject: [PATCH] feat(debugview): DV-1 native-binding linkage on AlarmStateChanged contract chain Add two additive init-only fields to AlarmStateChanged so the Debug View can nest live native conditions under their configured source-binding node: - NativeSourceCanonicalName (binding canonical name, e.g. "Motor1.MotorAlarms") - IsConfiguredPlaceholder (quiet-binding placeholder flag; default false) Flow on BOTH cross-process paths: - Live: proto AlarmStateUpdate fields 22/23 -> StreamRelayActor packs -> SiteStreamGrpcClient unpacks (regenerated SiteStreamGrpc/Sitestream.cs). - Snapshot (Newtonsoft): record defaults carry through; no special handling. NativeAlarmActor.Emit now stamps NativeSourceCanonicalName = _source.CanonicalName. Additive-only: no existing positional constructor or wire frame changed. Tests: StreamRelayActorTests round-trips both fields pack->unpack; NativeAlarmActorTests asserts the emitted event carries the binding canonical name. --- .../Messages/Streaming/AlarmStateChanged.cs | 14 ++ .../Actors/StreamRelayActor.cs | 4 +- .../Grpc/SiteStreamGrpcClient.cs | 4 +- .../Protos/sitestream.proto | 2 + .../SiteStreamGrpc/Sitestream.cs | 197 ++++++++++++------ .../Actors/NativeAlarmActor.cs | 3 +- .../Grpc/StreamRelayActorTests.cs | 46 ++++ .../Actors/NativeAlarmActorTests.cs | 21 ++ 8 files changed, 230 insertions(+), 61 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs index 7aa3fb61..ec2ddb37 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs @@ -71,4 +71,18 @@ public record AlarmStateChanged( /// Limit/threshold value for native limit alarms (display-only); empty otherwise. public string LimitValue { get; init; } = string.Empty; + + /// + /// Canonical name of the native alarm SOURCE BINDING this condition belongs to + /// (e.g. "Motor1.MotorAlarms"). Lets the Debug View nest live native conditions + /// under their configured binding node. Empty for computed alarms. Additive. + /// + public string NativeSourceCanonicalName { get; init; } = string.Empty; + + /// + /// True when this row is a placeholder emitted for a CONFIGURED native source + /// binding that currently has no active conditions, so the Debug View tree can + /// show the binding node as "no active conditions". Additive; default false. + /// + public bool IsConfiguredPlaceholder { get; init; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs index 4103640d..ffe6c6a0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs @@ -84,7 +84,9 @@ public class StreamRelayActor : ReceiveActor ? Timestamp.FromDateTimeOffset(msg.OriginalRaiseTime.Value) : null, CurrentValue = msg.CurrentValue ?? string.Empty, - LimitValue = msg.LimitValue ?? string.Empty + LimitValue = msg.LimitValue ?? string.Empty, + NativeSourceCanonicalName = msg.NativeSourceCanonicalName ?? string.Empty, + IsConfiguredPlaceholder = msg.IsConfiguredPlaceholder } }; diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs index c6eae35f..8e57e851 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs @@ -253,7 +253,9 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable OperatorComment = evt.AlarmChanged.OperatorComment ?? string.Empty, OriginalRaiseTime = evt.AlarmChanged.OriginalRaiseTime?.ToDateTimeOffset(), CurrentValue = evt.AlarmChanged.CurrentValue ?? string.Empty, - LimitValue = evt.AlarmChanged.LimitValue ?? string.Empty + LimitValue = evt.AlarmChanged.LimitValue ?? string.Empty, + NativeSourceCanonicalName = evt.AlarmChanged.NativeSourceCanonicalName ?? string.Empty, + IsConfiguredPlaceholder = evt.AlarmChanged.IsConfiguredPlaceholder }, _ => null }; diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto b/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto index df9ee7af..c76173b7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto @@ -84,6 +84,8 @@ message AlarmStateUpdate { google.protobuf.Timestamp original_raise_time = 19; // null when unknown string current_value = 20; string limit_value = 21; + string native_source_canonical_name = 22; // native binding canonical name; empty for computed + bool is_configured_placeholder = 23; // true for a quiet-binding placeholder row } // Audit Log (#23) telemetry: single lifecycle event ferried from a site SQLite diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs index a0e79003..f81bf8e2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs @@ -36,7 +36,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { "KAkSFgoOYXR0cmlidXRlX3BhdGgYAiABKAkSFgoOYXR0cmlidXRlX25hbWUY", "AyABKAkSDQoFdmFsdWUYBCABKAkSJAoHcXVhbGl0eRgFIAEoDjITLnNpdGVz", "dHJlYW0uUXVhbGl0eRItCgl0aW1lc3RhbXAYBiABKAsyGi5nb29nbGUucHJv", - "dG9idWYuVGltZXN0YW1wIrgEChBBbGFybVN0YXRlVXBkYXRlEhwKFGluc3Rh", + "dG9idWYuVGltZXN0YW1wIoEFChBBbGFybVN0YXRlVXBkYXRlEhwKFGluc3Rh", "bmNlX3VuaXF1ZV9uYW1lGAEgASgJEhIKCmFsYXJtX25hbWUYAiABKAkSKQoF", "c3RhdGUYAyABKA4yGi5zaXRlc3RyZWFtLkFsYXJtU3RhdGVFbnVtEhAKCHBy", "aW9yaXR5GAQgASgFEi0KCXRpbWVzdGFtcBgFIAEoCzIaLmdvb2dsZS5wcm90", @@ -49,69 +49,70 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { "c2VyGBEgASgJEhgKEG9wZXJhdG9yX2NvbW1lbnQYEiABKAkSNwoTb3JpZ2lu", "YWxfcmFpc2VfdGltZRgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh", "bXASFQoNY3VycmVudF92YWx1ZRgUIAEoCRITCgtsaW1pdF92YWx1ZRgVIAEo", - "CSK9BAoNQXVkaXRFdmVudER0bxIQCghldmVudF9pZBgBIAEoCRIzCg9vY2N1", - "cnJlZF9hdF91dGMYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1w", - "Eg8KB2NoYW5uZWwYAyABKAkSDAoEa2luZBgEIAEoCRIWCg5jb3JyZWxhdGlv", - "bl9pZBgFIAEoCRIWCg5zb3VyY2Vfc2l0ZV9pZBgGIAEoCRIaChJzb3VyY2Vf", - "aW5zdGFuY2VfaWQYByABKAkSFQoNc291cmNlX3NjcmlwdBgIIAEoCRINCgVh", - "Y3RvchgJIAEoCRIOCgZ0YXJnZXQYCiABKAkSDgoGc3RhdHVzGAsgASgJEjAK", - "C2h0dHBfc3RhdHVzGAwgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMyVmFs", - "dWUSMAoLZHVyYXRpb25fbXMYDSABKAsyGy5nb29nbGUucHJvdG9idWYuSW50", - "MzJWYWx1ZRIVCg1lcnJvcl9tZXNzYWdlGA4gASgJEhQKDGVycm9yX2RldGFp", - "bBgPIAEoCRIXCg9yZXF1ZXN0X3N1bW1hcnkYECABKAkSGAoQcmVzcG9uc2Vf", - "c3VtbWFyeRgRIAEoCRIZChFwYXlsb2FkX3RydW5jYXRlZBgSIAEoCBINCgVl", - "eHRyYRgTIAEoCRIUCgxleGVjdXRpb25faWQYFCABKAkSGwoTcGFyZW50X2V4", - "ZWN1dGlvbl9pZBgVIAEoCRITCgtzb3VyY2Vfbm9kZRgWIAEoCSI8Cg9BdWRp", - "dEV2ZW50QmF0Y2gSKQoGZXZlbnRzGAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRp", - "dEV2ZW50RHRvIicKCUluZ2VzdEFjaxIaChJhY2NlcHRlZF9ldmVudF9pZHMY", - "ASADKAkiiQMKFlNpdGVDYWxsT3BlcmF0aW9uYWxEdG8SHAoUdHJhY2tlZF9v", - "cGVyYXRpb25faWQYASABKAkSDwoHY2hhbm5lbBgCIAEoCRIOCgZ0YXJnZXQY", - "AyABKAkSEwoLc291cmNlX3NpdGUYBCABKAkSDgoGc3RhdHVzGAUgASgJEhMK", - "C3JldHJ5X2NvdW50GAYgASgFEhIKCmxhc3RfZXJyb3IYByABKAkSMAoLaHR0", - "cF9zdGF0dXMYCCABKAsyGy5nb29nbGUucHJvdG9idWYuSW50MzJWYWx1ZRIy", - "Cg5jcmVhdGVkX2F0X3V0YxgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1l", - "c3RhbXASMgoOdXBkYXRlZF9hdF91dGMYCiABKAsyGi5nb29nbGUucHJvdG9i", - "dWYuVGltZXN0YW1wEjMKD3Rlcm1pbmFsX2F0X3V0YxgLIAEoCzIaLmdvb2ds", - "ZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLc291cmNlX25vZGUYDCABKAkigAEK", - "FUNhY2hlZFRlbGVtZXRyeVBhY2tldBIuCgthdWRpdF9ldmVudBgBIAEoCzIZ", - "LnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxI3CgtvcGVyYXRpb25hbBgCIAEo", - "CzIiLnNpdGVzdHJlYW0uU2l0ZUNhbGxPcGVyYXRpb25hbER0byJKChRDYWNo", - "ZWRUZWxlbWV0cnlCYXRjaBIyCgdwYWNrZXRzGAEgAygLMiEuc2l0ZXN0cmVh", - "bS5DYWNoZWRUZWxlbWV0cnlQYWNrZXQiWwoWUHVsbEF1ZGl0RXZlbnRzUmVx", - "dWVzdBItCglzaW5jZV91dGMYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGlt", - "ZXN0YW1wEhIKCmJhdGNoX3NpemUYAiABKAUiXAoXUHVsbEF1ZGl0RXZlbnRz", - "UmVzcG9uc2USKQoGZXZlbnRzGAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRpdEV2", - "ZW50RHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIgASgIIlkKFFB1bGxTaXRlQ2Fs", - "bHNSZXF1ZXN0Ei0KCXNpbmNlX3V0YxgBIAEoCzIaLmdvb2dsZS5wcm90b2J1", - "Zi5UaW1lc3RhbXASEgoKYmF0Y2hfc2l6ZRgCIAEoBSJpChVQdWxsU2l0ZUNh", - "bGxzUmVzcG9uc2USOAoMb3BlcmF0aW9uYWxzGAEgAygLMiIuc2l0ZXN0cmVh", - "bS5TaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIg", - "ASgIKlwKB1F1YWxpdHkSFwoTUVVBTElUWV9VTlNQRUNJRklFRBAAEhAKDFFV", - "QUxJVFlfR09PRBABEhUKEVFVQUxJVFlfVU5DRVJUQUlOEAISDwoLUVVBTElU", - "WV9CQUQQAypdCg5BbGFybVN0YXRlRW51bRIbChdBTEFSTV9TVEFURV9VTlNQ", - "RUNJRklFRBAAEhYKEkFMQVJNX1NUQVRFX05PUk1BTBABEhYKEkFMQVJNX1NU", - "QVRFX0FDVElWRRACKoUBCg5BbGFybUxldmVsRW51bRIUChBBTEFSTV9MRVZF", - "TF9OT05FEAASEwoPQUxBUk1fTEVWRUxfTE9XEAESFwoTQUxBUk1fTEVWRUxf", - "TE9XX0xPVxACEhQKEEFMQVJNX0xFVkVMX0hJR0gQAxIZChVBTEFSTV9MRVZF", - "TF9ISUdIX0hJR0gQBDK3AwoRU2l0ZVN0cmVhbVNlcnZpY2USVQoRU3Vic2Ny", - "aWJlSW5zdGFuY2USIS5zaXRlc3RyZWFtLkluc3RhbmNlU3RyZWFtUmVxdWVz", - "dBobLnNpdGVzdHJlYW0uU2l0ZVN0cmVhbUV2ZW50MAESRwoRSW5nZXN0QXVk", - "aXRFdmVudHMSGy5zaXRlc3RyZWFtLkF1ZGl0RXZlbnRCYXRjaBoVLnNpdGVz", - "dHJlYW0uSW5nZXN0QWNrElAKFUluZ2VzdENhY2hlZFRlbGVtZXRyeRIgLnNp", - "dGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gaFS5zaXRlc3RyZWFtLklu", - "Z2VzdEFjaxJaCg9QdWxsQXVkaXRFdmVudHMSIi5zaXRlc3RyZWFtLlB1bGxB", - "dWRpdEV2ZW50c1JlcXVlc3QaIy5zaXRlc3RyZWFtLlB1bGxBdWRpdEV2ZW50", - "c1Jlc3BvbnNlElQKDVB1bGxTaXRlQ2FsbHMSIC5zaXRlc3RyZWFtLlB1bGxT", - "aXRlQ2FsbHNSZXF1ZXN0GiEuc2l0ZXN0cmVhbS5QdWxsU2l0ZUNhbGxzUmVz", - "cG9uc2VCK6oCKFpCLk1PTS5XVy5TY2FkYUJyaWRnZS5Db21tdW5pY2F0aW9u", - "LkdycGNiBnByb3RvMw==")); + "CRIkChxuYXRpdmVfc291cmNlX2Nhbm9uaWNhbF9uYW1lGBYgASgJEiEKGWlz", + "X2NvbmZpZ3VyZWRfcGxhY2Vob2xkZXIYFyABKAgivQQKDUF1ZGl0RXZlbnRE", + "dG8SEAoIZXZlbnRfaWQYASABKAkSMwoPb2NjdXJyZWRfYXRfdXRjGAIgASgL", + "MhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdjaGFubmVsGAMgASgJ", + "EgwKBGtpbmQYBCABKAkSFgoOY29ycmVsYXRpb25faWQYBSABKAkSFgoOc291", + "cmNlX3NpdGVfaWQYBiABKAkSGgoSc291cmNlX2luc3RhbmNlX2lkGAcgASgJ", + "EhUKDXNvdXJjZV9zY3JpcHQYCCABKAkSDQoFYWN0b3IYCSABKAkSDgoGdGFy", + "Z2V0GAogASgJEg4KBnN0YXR1cxgLIAEoCRIwCgtodHRwX3N0YXR1cxgMIAEo", + "CzIbLmdvb2dsZS5wcm90b2J1Zi5JbnQzMlZhbHVlEjAKC2R1cmF0aW9uX21z", + "GA0gASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSFQoNZXJyb3Jf", + "bWVzc2FnZRgOIAEoCRIUCgxlcnJvcl9kZXRhaWwYDyABKAkSFwoPcmVxdWVz", + "dF9zdW1tYXJ5GBAgASgJEhgKEHJlc3BvbnNlX3N1bW1hcnkYESABKAkSGQoR", + "cGF5bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkSFAoMZXhl", + "Y3V0aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAkS", + "EwoLc291cmNlX25vZGUYFiABKAkiPAoPQXVkaXRFdmVudEJhdGNoEikKBmV2", + "ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0byInCglJbmdl", + "c3RBY2sSGgoSYWNjZXB0ZWRfZXZlbnRfaWRzGAEgAygJIokDChZTaXRlQ2Fs", + "bE9wZXJhdGlvbmFsRHRvEhwKFHRyYWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJ", + "Eg8KB2NoYW5uZWwYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9z", + "aXRlGAQgASgJEg4KBnN0YXR1cxgFIAEoCRITCgtyZXRyeV9jb3VudBgGIAEo", + "BRISCgpsYXN0X2Vycm9yGAcgASgJEjAKC2h0dHBfc3RhdHVzGAggASgLMhsu", + "Z29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSMgoOY3JlYXRlZF9hdF91dGMY", + "CSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRf", + "YXRfdXRjGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCg90", + "ZXJtaW5hbF9hdF91dGMYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0", + "YW1wEhMKC3NvdXJjZV9ub2RlGAwgASgJIoABChVDYWNoZWRUZWxlbWV0cnlQ", + "YWNrZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0", + "RXZlbnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFtLlNp", + "dGVDYWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gS", + "MgoHcGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5", + "UGFja2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRj", + "GAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9z", + "aXplGAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2ZW50", + "cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3JlX2F2", + "YWlsYWJsZRgCIAEoCCJZChRQdWxsU2l0ZUNhbGxzUmVxdWVzdBItCglzaW5j", + "ZV91dGMYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhIKCmJh", + "dGNoX3NpemUYAiABKAUiaQoVUHVsbFNpdGVDYWxsc1Jlc3BvbnNlEjgKDG9w", + "ZXJhdGlvbmFscxgBIAMoCzIiLnNpdGVzdHJlYW0uU2l0ZUNhbGxPcGVyYXRp", + "b25hbER0bxIWCg5tb3JlX2F2YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcK", + "E1FVQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFR", + "VUFMSVRZX1VOQ0VSVEFJThACEg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1T", + "dGF0ZUVudW0SGwoXQUxBUk1fU1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFS", + "TV9TVEFURV9OT1JNQUwQARIWChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoO", + "QWxhcm1MZXZlbEVudW0SFAoQQUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJN", + "X0xFVkVMX0xPVxABEhcKE0FMQVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFS", + "TV9MRVZFTF9ISUdIEAMSGQoVQUxBUk1fTEVWRUxfSElHSF9ISUdIEAQytwMK", + "EVNpdGVTdHJlYW1TZXJ2aWNlElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0", + "ZXN0cmVhbS5JbnN0YW5jZVN0cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNp", + "dGVTdHJlYW1FdmVudDABEkcKEUluZ2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0", + "cmVhbS5BdWRpdEV2ZW50QmF0Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQ", + "ChVJbmdlc3RDYWNoZWRUZWxlbWV0cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRl", + "bGVtZXRyeUJhdGNoGhUuc2l0ZXN0cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1", + "ZGl0RXZlbnRzEiIuc2l0ZXN0cmVhbS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0", + "GiMuc2l0ZXN0cmVhbS5QdWxsQXVkaXRFdmVudHNSZXNwb25zZRJUCg1QdWxs", + "U2l0ZUNhbGxzEiAuc2l0ZXN0cmVhbS5QdWxsU2l0ZUNhbGxzUmVxdWVzdBoh", + "LnNpdGVzdHJlYW0uUHVsbFNpdGVDYWxsc1Jlc3BvbnNlQiuqAihaQi5NT00u", + "V1cuU2NhZGFCcmlkZ2UuQ29tbXVuaWNhdGlvbi5HcnBjYgZwcm90bzM=")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.Quality), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateEnum), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmLevelEnum), }, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest.Parser, new[]{ "CorrelationId", "InstanceUniqueName" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent.Parser, new[]{ "CorrelationId", "AttributeChanged", "AlarmChanged" }, new[]{ "Event" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AttributeValueUpdate), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AttributeValueUpdate.Parser, new[]{ "InstanceUniqueName", "AttributePath", "AttributeName", "Value", "Quality", "Timestamp" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate.Parser, new[]{ "InstanceUniqueName", "AlarmName", "State", "Priority", "Timestamp", "Level", "Message", "Kind", "Active", "Acknowledged", "Confirmed", "ShelveState", "Suppressed", "SourceReference", "AlarmTypeName", "Category", "OperatorUser", "OperatorComment", "OriginalRaiseTime", "CurrentValue", "LimitValue" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate.Parser, new[]{ "InstanceUniqueName", "AlarmName", "State", "Priority", "Timestamp", "Level", "Message", "Kind", "Active", "Acknowledged", "Confirmed", "ShelveState", "Suppressed", "SourceReference", "AlarmTypeName", "Category", "OperatorUser", "OperatorComment", "OriginalRaiseTime", "CurrentValue", "LimitValue", "NativeSourceCanonicalName", "IsConfiguredPlaceholder" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventDto), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ExecutionId", "ParentExecutionId", "SourceNode" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventBatch), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventBatch.Parser, new[]{ "Events" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck.Parser, new[]{ "AcceptedEventIds" }, null, null, null, null), @@ -1171,6 +1172,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { originalRaiseTime_ = other.originalRaiseTime_ != null ? other.originalRaiseTime_.Clone() : null; currentValue_ = other.currentValue_; limitValue_ = other.limitValue_; + nativeSourceCanonicalName_ = other.nativeSourceCanonicalName_; + isConfiguredPlaceholder_ = other.isConfiguredPlaceholder_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -1460,6 +1463,36 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { } } + /// Field number for the "native_source_canonical_name" field. + public const int NativeSourceCanonicalNameFieldNumber = 22; + private string nativeSourceCanonicalName_ = ""; + /// + /// native binding canonical name; empty for computed + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string NativeSourceCanonicalName { + get { return nativeSourceCanonicalName_; } + set { + nativeSourceCanonicalName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "is_configured_placeholder" field. + public const int IsConfiguredPlaceholderFieldNumber = 23; + private bool isConfiguredPlaceholder_; + /// + /// true for a quiet-binding placeholder row + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IsConfiguredPlaceholder { + get { return isConfiguredPlaceholder_; } + set { + isConfiguredPlaceholder_ = value; + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -1496,6 +1529,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { if (!object.Equals(OriginalRaiseTime, other.OriginalRaiseTime)) return false; if (CurrentValue != other.CurrentValue) return false; if (LimitValue != other.LimitValue) return false; + if (NativeSourceCanonicalName != other.NativeSourceCanonicalName) return false; + if (IsConfiguredPlaceholder != other.IsConfiguredPlaceholder) return false; return Equals(_unknownFields, other._unknownFields); } @@ -1524,6 +1559,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { if (originalRaiseTime_ != null) hash ^= OriginalRaiseTime.GetHashCode(); if (CurrentValue.Length != 0) hash ^= CurrentValue.GetHashCode(); if (LimitValue.Length != 0) hash ^= LimitValue.GetHashCode(); + if (NativeSourceCanonicalName.Length != 0) hash ^= NativeSourceCanonicalName.GetHashCode(); + if (IsConfiguredPlaceholder != false) hash ^= IsConfiguredPlaceholder.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -1626,6 +1663,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { output.WriteRawTag(170, 1); output.WriteString(LimitValue); } + if (NativeSourceCanonicalName.Length != 0) { + output.WriteRawTag(178, 1); + output.WriteString(NativeSourceCanonicalName); + } + if (IsConfiguredPlaceholder != false) { + output.WriteRawTag(184, 1); + output.WriteBool(IsConfiguredPlaceholder); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -1720,6 +1765,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { output.WriteRawTag(170, 1); output.WriteString(LimitValue); } + if (NativeSourceCanonicalName.Length != 0) { + output.WriteRawTag(178, 1); + output.WriteString(NativeSourceCanonicalName); + } + if (IsConfiguredPlaceholder != false) { + output.WriteRawTag(184, 1); + output.WriteBool(IsConfiguredPlaceholder); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -1793,6 +1846,12 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { if (LimitValue.Length != 0) { size += 2 + pb::CodedOutputStream.ComputeStringSize(LimitValue); } + if (NativeSourceCanonicalName.Length != 0) { + size += 2 + pb::CodedOutputStream.ComputeStringSize(NativeSourceCanonicalName); + } + if (IsConfiguredPlaceholder != false) { + size += 2 + 1; + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -1874,6 +1933,12 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { if (other.LimitValue.Length != 0) { LimitValue = other.LimitValue; } + if (other.NativeSourceCanonicalName.Length != 0) { + NativeSourceCanonicalName = other.NativeSourceCanonicalName; + } + if (other.IsConfiguredPlaceholder != false) { + IsConfiguredPlaceholder = other.IsConfiguredPlaceholder; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -1983,6 +2048,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { LimitValue = input.ReadString(); break; } + case 178: { + NativeSourceCanonicalName = input.ReadString(); + break; + } + case 184: { + IsConfiguredPlaceholder = input.ReadBool(); + break; + } } } #endif @@ -2092,6 +2165,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { LimitValue = input.ReadString(); break; } + case 178: { + NativeSourceCanonicalName = input.ReadString(); + break; + } + case 184: { + IsConfiguredPlaceholder = input.ReadBool(); + break; + } } } } diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs index 63a354ed..0a90be04 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs @@ -330,7 +330,8 @@ public class NativeAlarmActor : ReceiveActor OperatorComment = t.OperatorComment, OriginalRaiseTime = t.OriginalRaiseTime, CurrentValue = t.CurrentValue, - LimitValue = t.LimitValue + LimitValue = t.LimitValue, + NativeSourceCanonicalName = _source.CanonicalName, }; _instanceActor.Tell(change); diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs index ba1fad29..cbfeab58 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs @@ -113,6 +113,52 @@ public class StreamRelayActorTests : TestKit Assert.Equal("90", alarm.LimitValue); } + [Fact] + public void RelaysAlarmStateChanged_NativeBindingLinkage_SurvivesFullRoundTrip() + { + // DV-1: the native source-binding canonical name and the configured-placeholder + // flag must pack into AlarmStateUpdate (StreamRelayActor) and unpack back out + // (SiteStreamGrpcClient.ConvertToDomainEvent) — the full cross-process round trip. + var channel = Channel.CreateUnbounded(); + var actor = Sys.ActorOf(Props.Create(() => + new StreamRelayActor("corr-native-binding", channel.Writer))); + + var timestamp = new DateTimeOffset(2026, 3, 21, 11, 0, 0, TimeSpan.Zero); + var domainEvent = new AlarmStateChanged( + "Site1.Motor01", "Motor1.MotorAlarms.Hi", AlarmState.Active, 900, timestamp) + { + Kind = AlarmKind.NativeOpcUa, + SourceReference = "Motor1.MotorAlarms.Hi", + NativeSourceCanonicalName = "Motor1.MotorAlarms", + IsConfiguredPlaceholder = true, + Condition = new AlarmConditionState( + Active: true, Acknowledged: false, Confirmed: null, + Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 900) + }; + + actor.Tell(domainEvent); + + var success = channel.Reader.TryRead(out var protoEvent); + if (!success) + { + Thread.Sleep(500); + success = channel.Reader.TryRead(out protoEvent); + } + + Assert.True(success, "Expected a proto event on the channel"); + Assert.NotNull(protoEvent); + + // Packed onto the wire frame by StreamRelayActor. + Assert.Equal("Motor1.MotorAlarms", protoEvent.AlarmChanged.NativeSourceCanonicalName); + Assert.True(protoEvent.AlarmChanged.IsConfiguredPlaceholder); + + // Unpacked back into the domain record by SiteStreamGrpcClient. + var roundTripped = Assert.IsType( + SiteStreamGrpcClient.ConvertToDomainEvent(protoEvent)); + Assert.Equal("Motor1.MotorAlarms", roundTripped.NativeSourceCanonicalName); + Assert.True(roundTripped.IsConfiguredPlaceholder); + } + [Fact] public void SetsCorrelationId_OnAllEvents() { diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs index abcd0766..3c3a45c8 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs @@ -79,6 +79,27 @@ public class NativeAlarmActorTests : TestKit, IDisposable Assert.Equal(800, emitted.Condition.Severity); } + [Fact] + public void Raise_StampsNativeSourceCanonicalName_FromConfiguredBinding() + { + // DV-1: every emitted condition carries the canonical name of the source + // BINDING it belongs to, so the DebugView can nest live native conditions + // under their configured binding node. It must equal the binding's + // CanonicalName used to construct the actor (Source().CanonicalName). + var instance = CreateTestProbe(); + var dcl = CreateTestProbe(); + var actor = Spawn(instance.Ref, dcl.Ref); + dcl.ExpectMsg(); + + actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( + "T01.Hi", AlarmTransitionKind.Raise, + new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)))); + + var emitted = instance.ExpectMsg(); + Assert.Equal(Source().CanonicalName, emitted.NativeSourceCanonicalName); + Assert.Equal("Pressure", emitted.NativeSourceCanonicalName); + } + [Fact] public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal() {