diff --git a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs
index 8ba521a..4c5cdbb 100644
--- a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs
+++ b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs
@@ -322,163 +322,166 @@ namespace MxGateway.Contracts.Proto {
"b29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJhdG9yX3VzZXIYCiAB",
"KAkSGAoQb3BlcmF0b3JfY29tbWVudBgLIAEoCRIzCg1jdXJyZW50X3ZhbHVl",
"GAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEjEKC2xpbWl0",
- "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIpIB",
- "ChdBY2tub3dsZWRnZUFsYXJtUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJ",
- "Eh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgCIAEoCRIcChRhbGFybV9mdWxs",
- "X3JlZmVyZW5jZRgDIAEoCRIPCgdjb21tZW50GAQgASgJEhUKDW9wZXJhdG9y",
- "X3VzZXIYBSABKAki8wEKFUFja25vd2xlZGdlQWxhcm1SZXBseRISCgpzZXNz",
- "aW9uX2lkGAEgASgJEhYKDmNvcnJlbGF0aW9uX2lkGAIgASgJEjwKD3Byb3Rv",
- "Y29sX3N0YXR1cxgDIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9j",
- "b2xTdGF0dXMSFAoHaHJlc3VsdBgEIAEoBUgAiAEBEjIKBnN0YXR1cxgFIAEo",
- "CzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQcm94eRIaChJkaWFn",
- "bm9zdGljX21lc3NhZ2UYBiABKAlCCgoIX2hyZXN1bHQiagoYUXVlcnlBY3Rp",
- "dmVBbGFybXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50",
- "X2NvcnJlbGF0aW9uX2lkGAIgASgJEhsKE2FsYXJtX2ZpbHRlcl9wcmVmaXgY",
- "AyABKAki6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3Cghj",
- "YXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXND",
- "YXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3",
- "YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19j",
- "YXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlh",
- "Z25vc3RpY190ZXh0GAcgASgJIqcDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgB",
- "IAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJp",
- "YW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25v",
- "c3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFs",
- "dWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3Zh",
- "bHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVf",
- "dmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1l",
- "c3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1w",
- "SAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYx",
- "Lk14QXJyYXlIABITCglyYXdfdmFsdWUYEiABKAxIAEIGCgRraW5kIv4ECgdN",
- "eEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3Nf",
- "Z2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIS",
- "CgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0K",
- "FXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgK",
- "IAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50",
- "MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFy",
- "cmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdh",
- "eS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhh",
- "Y2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVl",
- "cxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5",
- "Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5T",
- "dHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFj",
- "Y2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVl",
- "cxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2",
- "YWx1ZXMiGwoJQm9vbEFycmF5Eg4KBnZhbHVlcxgBIAMoCCIcCgpJbnQzMkFy",
- "cmF5Eg4KBnZhbHVlcxgBIAMoBSIcCgpJbnQ2NEFycmF5Eg4KBnZhbHVlcxgB",
- "IAMoAyIcCgpGbG9hdEFycmF5Eg4KBnZhbHVlcxgBIAMoAiIdCgtEb3VibGVB",
- "cnJheRIOCgZ2YWx1ZXMYASADKAEiHQoLU3RyaW5nQXJyYXkSDgoGdmFsdWVz",
- "GAEgAygJIjwKDlRpbWVzdGFtcEFycmF5EioKBnZhbHVlcxgBIAMoCzIaLmdv",
- "b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiGgoIUmF3QXJyYXkSDgoGdmFsdWVz",
- "GAEgAygMIlgKDlByb3RvY29sU3RhdHVzEjUKBGNvZGUYASABKA4yJy5teGFj",
- "Y2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVzQ29kZRIPCgdtZXNzYWdl",
- "GAIgASgJKp8LCg1NeENvbW1hbmRLaW5kEh8KG01YX0NPTU1BTkRfS0lORF9V",
- "TlNQRUNJRklFRBAAEhwKGE1YX0NPTU1BTkRfS0lORF9SRUdJU1RFUhABEh4K",
- "Gk1YX0NPTU1BTkRfS0lORF9VTlJFR0lTVEVSEAISHAoYTVhfQ09NTUFORF9L",
- "SU5EX0FERF9JVEVNEAMSHQoZTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNMhAE",
- "Eh8KG01YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTRAFEhoKFk1YX0NPTU1B",
- "TkRfS0lORF9BRFZJU0UQBhIdChlNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNF",
- "EAcSJgoiTVhfQ09NTUFORF9LSU5EX0FEVklTRV9TVVBFUlZJU09SWRAIEiUK",
- "IU1YX0NPTU1BTkRfS0lORF9BRERfQlVGRkVSRURfSVRFTRAJEjAKLE1YX0NP",
- "TU1BTkRfS0lORF9TRVRfQlVGRkVSRURfVVBEQVRFX0lOVEVSVkFMEAoSGwoX",
- "TVhfQ09NTUFORF9LSU5EX1NVU1BFTkQQCxIcChhNWF9DT01NQU5EX0tJTkRf",
- "QUNUSVZBVEUQDBIZChVNWF9DT01NQU5EX0tJTkRfV1JJVEUQDRIaChZNWF9D",
- "T01NQU5EX0tJTkRfV1JJVEUyEA4SIQodTVhfQ09NTUFORF9LSU5EX1dSSVRF",
- "X1NFQ1VSRUQQDxIiCh5NWF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDIQ",
- "EBIlCiFNWF9DT01NQU5EX0tJTkRfQVVUSEVOVElDQVRFX1VTRVIQERIoCiRN",
- "WF9DT01NQU5EX0tJTkRfQVJDSEVTVFJBX1VTRVJfVE9fSUQQEhIhCh1NWF9D",
- "T01NQU5EX0tJTkRfQUREX0lURU1fQlVMSxATEiQKIE1YX0NPTU1BTkRfS0lO",
- "RF9BRFZJU0VfSVRFTV9CVUxLEBQSJAogTVhfQ09NTUFORF9LSU5EX1JFTU9W",
- "RV9JVEVNX0JVTEsQFRInCiNNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFX0lU",
- "RU1fQlVMSxAWEiIKHk1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQlVMSxAX",
- "EiQKIE1YX0NPTU1BTkRfS0lORF9VTlNVQlNDUklCRV9CVUxLEBgSJAogTVhf",
- "Q09NTUFORF9LSU5EX1NVQlNDUklCRV9BTEFSTVMQGRImCiJNWF9DT01NQU5E",
- "X0tJTkRfVU5TVUJTQ1JJQkVfQUxBUk1TEBoSJQohTVhfQ09NTUFORF9LSU5E",
- "X0FDS05PV0xFREdFX0FMQVJNEBsSJwojTVhfQ09NTUFORF9LSU5EX1FVRVJZ",
- "X0FDVElWRV9BTEFSTVMQHBItCilNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVE",
- "R0VfQUxBUk1fQllfTkFNRRAdEh4KGk1YX0NPTU1BTkRfS0lORF9XUklURV9C",
- "VUxLEB4SHwobTVhfQ09NTUFORF9LSU5EX1dSSVRFMl9CVUxLEB8SJgoiTVhf",
- "Q09NTUFORF9LSU5EX1dSSVRFX1NFQ1VSRURfQlVMSxAgEicKI01YX0NPTU1B",
- "TkRfS0lORF9XUklURV9TRUNVUkVEMl9CVUxLECESHQoZTVhfQ09NTUFORF9L",
- "SU5EX1JFQURfQlVMSxAiEhgKFE1YX0NPTU1BTkRfS0lORF9QSU5HEGQSJQoh",
- "TVhfQ09NTUFORF9LSU5EX0dFVF9TRVNTSU9OX1NUQVRFEGUSIwofTVhfQ09N",
- "TUFORF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBmEiAKHE1YX0NPTU1BTkRfS0lO",
- "RF9EUkFJTl9FVkVOVFMQZxIjCh9NWF9DT01NQU5EX0tJTkRfU0hVVERPV05f",
- "V09SS0VSEGgq+QEKDU14RXZlbnRGYW1pbHkSHwobTVhfRVZFTlRfRkFNSUxZ",
- "X1VOU1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRfRkFNSUxZX09OX0RBVEFfQ0hB",
- "TkdFEAESJQohTVhfRVZFTlRfRkFNSUxZX09OX1dSSVRFX0NPTVBMRVRFEAIS",
- "JgoiTVhfRVZFTlRfRkFNSUxZX09QRVJBVElPTl9DT01QTEVURRADEisKJ01Y",
- "X0VWRU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9EQVRBX0NIQU5HRRAEEicKI01Y",
- "X0VWRU5UX0ZBTUlMWV9PTl9BTEFSTV9UUkFOU0lUSU9OEAUqygEKE0FsYXJt",
- "VHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BF",
- "Q0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQoh",
- "QUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1f",
- "VFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9L",
- "SU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFM",
- "QVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NP",
- "TkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RB",
- "VEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lO",
- "QUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NB",
- "VEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VO",
- "S05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFU",
- "VVNfQ0FURUdPUllfUEVORElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9X",
- "QVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05f",
- "RVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9F",
- "UlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJP",
- "UhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUK",
- "IU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NU",
- "QVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJj",
- "ZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RB",
- "VFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFV",
- "RVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19M",
- "TVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwof",
- "TVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRV",
- "U19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1Y",
- "X1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAH",
- "Kt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAA",
- "EhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05P",
- "X0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFf",
- "VFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhf",
- "REFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIV",
- "ChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VE",
- "X1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhN",
- "WF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5V",
- "TRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9F",
- "TlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwob",
- "TVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBF",
- "X1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJ",
- "T05BTElaRURfU1RSSU5HEBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQ",
- "EhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29k",
- "ZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BS",
- "T1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NP",
- "REVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVf",
- "U0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9T",
- "RVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dP",
- "UktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJ",
- "TUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisK",
- "J1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikK",
- "JVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoM",
- "U2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIa",
- "ChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9T",
- "VEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9S",
- "X1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VT",
- "U0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9T",
- "VEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNF",
- "U1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVE",
- "EAky4AQKD014QWNjZXNzR2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNj",
- "ZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3Nf",
- "Z2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIo",
- "Lm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14",
- "YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tl",
- "EiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhh",
- "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVu",
- "dHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3Qa",
- "HC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRn",
- "ZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJt",
- "UmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFy",
- "bVJlcGx5Em4KEVF1ZXJ5QWN0aXZlQWxhcm1zEi0ubXhhY2Nlc3NfZ2F0ZXdh",
- "eS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QaKC5teGFjY2Vzc19nYXRl",
- "d2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3QwAUIcqgIZTXhHYXRld2F5LkNv",
- "bnRyYWN0cy5Qcm90b2IGcHJvdG8z"));
+ "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIpAB",
+ "ChdBY2tub3dsZWRnZUFsYXJtUmVxdWVzdBIdChVjbGllbnRfY29ycmVsYXRp",
+ "b25faWQYAiABKAkSHAoUYWxhcm1fZnVsbF9yZWZlcmVuY2UYAyABKAkSDwoH",
+ "Y29tbWVudBgEIAEoCRIVCg1vcGVyYXRvcl91c2VyGAUgASgJSgQIARACUgpz",
+ "ZXNzaW9uX2lkIvEBChVBY2tub3dsZWRnZUFsYXJtUmVwbHkSFgoOY29ycmVs",
+ "YXRpb25faWQYAiABKAkSPAoPcHJvdG9jb2xfc3RhdHVzGAMgASgLMiMubXhh",
+ "Y2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0GAQg",
+ "ASgFSACIAQESMgoGc3RhdHVzGAUgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52",
+ "MS5NeFN0YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVzc2FnZRgGIAEoCUIK",
+ "CghfaHJlc3VsdEoECAEQAlIKc2Vzc2lvbl9pZCJRChNTdHJlYW1BbGFybXNS",
+ "ZXF1ZXN0Eh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgBIAEoCRIbChNhbGFy",
+ "bV9maWx0ZXJfcHJlZml4GAIgASgJIr8BChBBbGFybUZlZWRNZXNzYWdlEkAK",
+ "DGFjdGl2ZV9hbGFybRgBIAEoCzIoLm14YWNjZXNzX2dhdGV3YXkudjEuQWN0",
+ "aXZlQWxhcm1TbmFwc2hvdEgAEhsKEXNuYXBzaG90X2NvbXBsZXRlGAIgASgI",
+ "SAASQQoKdHJhbnNpdGlvbhgDIAEoCzIrLm14YWNjZXNzX2dhdGV3YXkudjEu",
+ "T25BbGFybVRyYW5zaXRpb25FdmVudEgAQgkKB3BheWxvYWQi6wEKDU14U3Rh",
+ "dHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3CghjYXRlZ29yeRgCIAEoDjIl",
+ "Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRl",
+ "Y3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNT",
+ "b3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19jYXRlZ29yeRgFIAEoBRIX",
+ "Cg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3RpY190ZXh0GAcg",
+ "ASgJIqcDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNz",
+ "X2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5cGUYAiABKAkS",
+ "DwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1y",
+ "YXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFsdWUYCiABKAhIABIVCgtp",
+ "bnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwgASgDSAASFQoL",
+ "ZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVfdmFsdWUYDiABKAFIABIW",
+ "CgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1lc3RhbXBfdmFsdWUYECAB",
+ "KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAASMwoLYXJyYXlfdmFs",
+ "dWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJyYXlIABITCgly",
+ "YXdfdmFsdWUYEiABKAxIAEIGCgRraW5kIv4ECgdNeEFycmF5EjoKEWVsZW1l",
+ "bnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERh",
+ "dGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRISCgpkaW1lbnNpb25zGAMg",
+ "AygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0KFXJhd19lbGVtZW50X2Rh",
+ "dGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNz",
+ "X2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50MzJfdmFsdWVzGAsgASgL",
+ "Mh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFycmF5SAASNwoMaW50NjRf",
+ "dmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5",
+ "SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52",
+ "MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNj",
+ "ZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVz",
+ "GA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5TdHJpbmdBcnJheUgAEj8K",
+ "EHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYx",
+ "LlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNj",
+ "ZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2YWx1ZXMiGwoJQm9vbEFy",
+ "cmF5Eg4KBnZhbHVlcxgBIAMoCCIcCgpJbnQzMkFycmF5Eg4KBnZhbHVlcxgB",
+ "IAMoBSIcCgpJbnQ2NEFycmF5Eg4KBnZhbHVlcxgBIAMoAyIcCgpGbG9hdEFy",
+ "cmF5Eg4KBnZhbHVlcxgBIAMoAiIdCgtEb3VibGVBcnJheRIOCgZ2YWx1ZXMY",
+ "ASADKAEiHQoLU3RyaW5nQXJyYXkSDgoGdmFsdWVzGAEgAygJIjwKDlRpbWVz",
+ "dGFtcEFycmF5EioKBnZhbHVlcxgBIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5U",
+ "aW1lc3RhbXAiGgoIUmF3QXJyYXkSDgoGdmFsdWVzGAEgAygMIlgKDlByb3Rv",
+ "Y29sU3RhdHVzEjUKBGNvZGUYASABKA4yJy5teGFjY2Vzc19nYXRld2F5LnYx",
+ "LlByb3RvY29sU3RhdHVzQ29kZRIPCgdtZXNzYWdlGAIgASgJKp8LCg1NeENv",
+ "bW1hbmRLaW5kEh8KG01YX0NPTU1BTkRfS0lORF9VTlNQRUNJRklFRBAAEhwK",
+ "GE1YX0NPTU1BTkRfS0lORF9SRUdJU1RFUhABEh4KGk1YX0NPTU1BTkRfS0lO",
+ "RF9VTlJFR0lTVEVSEAISHAoYTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNEAMS",
+ "HQoZTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNMhAEEh8KG01YX0NPTU1BTkRf",
+ "S0lORF9SRU1PVkVfSVRFTRAFEhoKFk1YX0NPTU1BTkRfS0lORF9BRFZJU0UQ",
+ "BhIdChlNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFEAcSJgoiTVhfQ09NTUFO",
+ "RF9LSU5EX0FEVklTRV9TVVBFUlZJU09SWRAIEiUKIU1YX0NPTU1BTkRfS0lO",
+ "RF9BRERfQlVGRkVSRURfSVRFTRAJEjAKLE1YX0NPTU1BTkRfS0lORF9TRVRf",
+ "QlVGRkVSRURfVVBEQVRFX0lOVEVSVkFMEAoSGwoXTVhfQ09NTUFORF9LSU5E",
+ "X1NVU1BFTkQQCxIcChhNWF9DT01NQU5EX0tJTkRfQUNUSVZBVEUQDBIZChVN",
+ "WF9DT01NQU5EX0tJTkRfV1JJVEUQDRIaChZNWF9DT01NQU5EX0tJTkRfV1JJ",
+ "VEUyEA4SIQodTVhfQ09NTUFORF9LSU5EX1dSSVRFX1NFQ1VSRUQQDxIiCh5N",
+ "WF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDIQEBIlCiFNWF9DT01NQU5E",
+ "X0tJTkRfQVVUSEVOVElDQVRFX1VTRVIQERIoCiRNWF9DT01NQU5EX0tJTkRf",
+ "QVJDSEVTVFJBX1VTRVJfVE9fSUQQEhIhCh1NWF9DT01NQU5EX0tJTkRfQURE",
+ "X0lURU1fQlVMSxATEiQKIE1YX0NPTU1BTkRfS0lORF9BRFZJU0VfSVRFTV9C",
+ "VUxLEBQSJAogTVhfQ09NTUFORF9LSU5EX1JFTU9WRV9JVEVNX0JVTEsQFRIn",
+ "CiNNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFX0lURU1fQlVMSxAWEiIKHk1Y",
+ "X0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQlVMSxAXEiQKIE1YX0NPTU1BTkRf",
+ "S0lORF9VTlNVQlNDUklCRV9CVUxLEBgSJAogTVhfQ09NTUFORF9LSU5EX1NV",
+ "QlNDUklCRV9BTEFSTVMQGRImCiJNWF9DT01NQU5EX0tJTkRfVU5TVUJTQ1JJ",
+ "QkVfQUxBUk1TEBoSJQohTVhfQ09NTUFORF9LSU5EX0FDS05PV0xFREdFX0FM",
+ "QVJNEBsSJwojTVhfQ09NTUFORF9LSU5EX1FVRVJZX0FDVElWRV9BTEFSTVMQ",
+ "HBItCilNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVER0VfQUxBUk1fQllfTkFN",
+ "RRAdEh4KGk1YX0NPTU1BTkRfS0lORF9XUklURV9CVUxLEB4SHwobTVhfQ09N",
+ "TUFORF9LSU5EX1dSSVRFMl9CVUxLEB8SJgoiTVhfQ09NTUFORF9LSU5EX1dS",
+ "SVRFX1NFQ1VSRURfQlVMSxAgEicKI01YX0NPTU1BTkRfS0lORF9XUklURV9T",
+ "RUNVUkVEMl9CVUxLECESHQoZTVhfQ09NTUFORF9LSU5EX1JFQURfQlVMSxAi",
+ "EhgKFE1YX0NPTU1BTkRfS0lORF9QSU5HEGQSJQohTVhfQ09NTUFORF9LSU5E",
+ "X0dFVF9TRVNTSU9OX1NUQVRFEGUSIwofTVhfQ09NTUFORF9LSU5EX0dFVF9X",
+ "T1JLRVJfSU5GTxBmEiAKHE1YX0NPTU1BTkRfS0lORF9EUkFJTl9FVkVOVFMQ",
+ "ZxIjCh9NWF9DT01NQU5EX0tJTkRfU0hVVERPV05fV09SS0VSEGgq+QEKDU14",
+ "RXZlbnRGYW1pbHkSHwobTVhfRVZFTlRfRkFNSUxZX1VOU1BFQ0lGSUVEEAAS",
+ "IgoeTVhfRVZFTlRfRkFNSUxZX09OX0RBVEFfQ0hBTkdFEAESJQohTVhfRVZF",
+ "TlRfRkFNSUxZX09OX1dSSVRFX0NPTVBMRVRFEAISJgoiTVhfRVZFTlRfRkFN",
+ "SUxZX09QRVJBVElPTl9DT01QTEVURRADEisKJ01YX0VWRU5UX0ZBTUlMWV9P",
+ "Tl9CVUZGRVJFRF9EQVRBX0NIQU5HRRAEEicKI01YX0VWRU5UX0ZBTUlMWV9P",
+ "Tl9BTEFSTV9UUkFOU0lUSU9OEAUqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQS",
+ "JQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxB",
+ "Uk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElP",
+ "Tl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5E",
+ "X0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAE",
+ "KqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9T",
+ "VEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9B",
+ "Q1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VE",
+ "EAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14",
+ "U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lG",
+ "SUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9T",
+ "VEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVO",
+ "RElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhf",
+ "U1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9T",
+ "VEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NU",
+ "QVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRV",
+ "U19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRF",
+ "R09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9P",
+ "VEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNf",
+ "U09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktO",
+ "T1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMK",
+ "H01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFU",
+ "VVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJD",
+ "RV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVT",
+ "VElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0Vf",
+ "UkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBl",
+ "EhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQ",
+ "RV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9E",
+ "QVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQS",
+ "FgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJM",
+ "RRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVf",
+ "VElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9E",
+ "QVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RB",
+ "VFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFf",
+ "VFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFU",
+ "QV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FV",
+ "QUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJV",
+ "Q1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5H",
+ "EBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZ",
+ "UEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9T",
+ "VEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19D",
+ "T0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFV",
+ "RVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9V",
+ "TkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFE",
+ "WRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJM",
+ "RRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9U",
+ "T0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRV",
+ "U19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRV",
+ "U19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0K",
+ "GVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRF",
+ "X0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQ",
+ "AhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNT",
+ "SU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklU",
+ "SUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkK",
+ "FVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xP",
+ "U0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVEEAky0wQKD014QWNjZXNz",
+ "R2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEu",
+ "T3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVu",
+ "U2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3",
+ "YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXku",
+ "djEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0",
+ "ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52",
+ "MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19n",
+ "YXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRl",
+ "d2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nl",
+ "c3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNj",
+ "ZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFybVJlcGx5EmEKDFN0cmVh",
+ "bUFsYXJtcxIoLm14YWNjZXNzX2dhdGV3YXkudjEuU3RyZWFtQWxhcm1zUmVx",
+ "dWVzdBolLm14YWNjZXNzX2dhdGV3YXkudjEuQWxhcm1GZWVkTWVzc2FnZTAB",
+ "QhyqAhlNeEdhdGV3YXkuQ29udHJhY3RzLlByb3RvYgZwcm90bzM="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] {
@@ -559,9 +562,10 @@ namespace MxGateway.Contracts.Proto {
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnBufferedDataChangeEvent), global::MxGateway.Contracts.Proto.OnBufferedDataChangeEvent.Parser, new[]{ "DataType", "QualityValues", "TimestampValues", "RawDataType" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent), global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent.Parser, new[]{ "AlarmFullReference", "SourceObjectReference", "AlarmTypeName", "TransitionKind", "Severity", "OriginalRaiseTimestamp", "TransitionTimestamp", "OperatorUser", "OperatorComment", "Category", "Description", "CurrentValue", "LimitValue" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot), global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser, new[]{ "AlarmFullReference", "SourceObjectReference", "AlarmTypeName", "Severity", "OriginalRaiseTimestamp", "CurrentState", "Category", "Description", "LastTransitionTimestamp", "OperatorUser", "OperatorComment", "CurrentValue", "LimitValue" }, null, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "SessionId", "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest), global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamAlarmsRequest), global::MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AlarmFeedMessage), global::MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition" }, new[]{ "Payload" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxStatusProxy), global::MxGateway.Contracts.Proto.MxStatusProxy.Parser, new[]{ "Success", "Category", "DetectedBy", "Detail", "RawCategory", "RawDetectedBy", "DiagnosticText" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxValue), global::MxGateway.Contracts.Proto.MxValue.Parser, new[]{ "DataType", "VariantType", "IsNull", "RawDiagnostic", "RawDataType", "BoolValue", "Int32Value", "Int64Value", "FloatValue", "DoubleValue", "StringValue", "TimestampValue", "ArrayValue", "RawValue" }, new[]{ "Kind" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxArray), global::MxGateway.Contracts.Proto.MxArray.Parser, new[]{ "ElementDataType", "VariantType", "Dimensions", "RawDiagnostic", "RawElementDataType", "BoolValues", "Int32Values", "Int64Values", "FloatValues", "DoubleValues", "StringValues", "TimestampValues", "RawValues" }, new[]{ "Values" }, null, null, null),
@@ -25428,7 +25432,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public AcknowledgeAlarmRequest(AcknowledgeAlarmRequest other) : this() {
- sessionId_ = other.sessionId_;
clientCorrelationId_ = other.clientCorrelationId_;
alarmFullReference_ = other.alarmFullReference_;
comment_ = other.comment_;
@@ -25442,18 +25445,6 @@ namespace MxGateway.Contracts.Proto {
return new AcknowledgeAlarmRequest(this);
}
- /// Field number for the "session_id" field.
- public const int SessionIdFieldNumber = 1;
- private string sessionId_ = "";
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
- [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public string SessionId {
- get { return sessionId_; }
- set {
- sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
- }
- }
-
/// Field number for the "client_correlation_id" field.
public const int ClientCorrelationIdFieldNumber = 2;
private string clientCorrelationId_ = "";
@@ -25527,7 +25518,6 @@ namespace MxGateway.Contracts.Proto {
if (ReferenceEquals(other, this)) {
return true;
}
- if (SessionId != other.SessionId) return false;
if (ClientCorrelationId != other.ClientCorrelationId) return false;
if (AlarmFullReference != other.AlarmFullReference) return false;
if (Comment != other.Comment) return false;
@@ -25539,7 +25529,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
- if (SessionId.Length != 0) hash ^= SessionId.GetHashCode();
if (ClientCorrelationId.Length != 0) hash ^= ClientCorrelationId.GetHashCode();
if (AlarmFullReference.Length != 0) hash ^= AlarmFullReference.GetHashCode();
if (Comment.Length != 0) hash ^= Comment.GetHashCode();
@@ -25562,10 +25551,6 @@ namespace MxGateway.Contracts.Proto {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
output.WriteRawTag(18);
output.WriteString(ClientCorrelationId);
@@ -25592,10 +25577,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
output.WriteRawTag(18);
output.WriteString(ClientCorrelationId);
@@ -25622,9 +25603,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
- if (SessionId.Length != 0) {
- size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(ClientCorrelationId);
}
@@ -25649,9 +25627,6 @@ namespace MxGateway.Contracts.Proto {
if (other == null) {
return;
}
- if (other.SessionId.Length != 0) {
- SessionId = other.SessionId;
- }
if (other.ClientCorrelationId.Length != 0) {
ClientCorrelationId = other.ClientCorrelationId;
}
@@ -25683,10 +25658,6 @@ namespace MxGateway.Contracts.Proto {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
- case 10: {
- SessionId = input.ReadString();
- break;
- }
case 18: {
ClientCorrelationId = input.ReadString();
break;
@@ -25722,10 +25693,6 @@ namespace MxGateway.Contracts.Proto {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
- case 10: {
- SessionId = input.ReadString();
- break;
- }
case 18: {
ClientCorrelationId = input.ReadString();
break;
@@ -25786,7 +25753,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public AcknowledgeAlarmReply(AcknowledgeAlarmReply other) : this() {
_hasBits0 = other._hasBits0;
- sessionId_ = other.sessionId_;
correlationId_ = other.correlationId_;
protocolStatus_ = other.protocolStatus_ != null ? other.protocolStatus_.Clone() : null;
hresult_ = other.hresult_;
@@ -25801,18 +25767,6 @@ namespace MxGateway.Contracts.Proto {
return new AcknowledgeAlarmReply(this);
}
- /// Field number for the "session_id" field.
- public const int SessionIdFieldNumber = 1;
- private string sessionId_ = "";
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
- [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public string SessionId {
- get { return sessionId_; }
- set {
- sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
- }
- }
-
/// Field number for the "correlation_id" field.
public const int CorrelationIdFieldNumber = 2;
private string correlationId_ = "";
@@ -25918,7 +25872,6 @@ namespace MxGateway.Contracts.Proto {
if (ReferenceEquals(other, this)) {
return true;
}
- if (SessionId != other.SessionId) return false;
if (CorrelationId != other.CorrelationId) return false;
if (!object.Equals(ProtocolStatus, other.ProtocolStatus)) return false;
if (Hresult != other.Hresult) return false;
@@ -25931,7 +25884,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
- if (SessionId.Length != 0) hash ^= SessionId.GetHashCode();
if (CorrelationId.Length != 0) hash ^= CorrelationId.GetHashCode();
if (protocolStatus_ != null) hash ^= ProtocolStatus.GetHashCode();
if (HasHresult) hash ^= Hresult.GetHashCode();
@@ -25955,10 +25907,6 @@ namespace MxGateway.Contracts.Proto {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (CorrelationId.Length != 0) {
output.WriteRawTag(18);
output.WriteString(CorrelationId);
@@ -25989,10 +25937,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (CorrelationId.Length != 0) {
output.WriteRawTag(18);
output.WriteString(CorrelationId);
@@ -26023,9 +25967,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
- if (SessionId.Length != 0) {
- size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId);
- }
if (CorrelationId.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(CorrelationId);
}
@@ -26053,9 +25994,6 @@ namespace MxGateway.Contracts.Proto {
if (other == null) {
return;
}
- if (other.SessionId.Length != 0) {
- SessionId = other.SessionId;
- }
if (other.CorrelationId.Length != 0) {
CorrelationId = other.CorrelationId;
}
@@ -26096,10 +26034,6 @@ namespace MxGateway.Contracts.Proto {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
- case 10: {
- SessionId = input.ReadString();
- break;
- }
case 18: {
CorrelationId = input.ReadString();
break;
@@ -26145,10 +26079,6 @@ namespace MxGateway.Contracts.Proto {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
- case 10: {
- SessionId = input.ReadString();
- break;
- }
case 18: {
CorrelationId = input.ReadString();
break;
@@ -26182,17 +26112,20 @@ namespace MxGateway.Contracts.Proto {
}
+ ///
+ /// Request to attach to the gateway's central alarm feed (StreamAlarms).
+ ///
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
- public sealed partial class QueryActiveAlarmsRequest : pb::IMessage
+ public sealed partial class StreamAlarmsRequest : pb::IMessage
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
- private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new QueryActiveAlarmsRequest());
+ private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new StreamAlarmsRequest());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public static pb::MessageParser Parser { get { return _parser; } }
+ public static pb::MessageParser Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
@@ -26208,7 +26141,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public QueryActiveAlarmsRequest() {
+ public StreamAlarmsRequest() {
OnConstruction();
}
@@ -26216,8 +26149,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public QueryActiveAlarmsRequest(QueryActiveAlarmsRequest other) : this() {
- sessionId_ = other.sessionId_;
+ public StreamAlarmsRequest(StreamAlarmsRequest other) : this() {
clientCorrelationId_ = other.clientCorrelationId_;
alarmFilterPrefix_ = other.alarmFilterPrefix_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
@@ -26225,24 +26157,12 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public QueryActiveAlarmsRequest Clone() {
- return new QueryActiveAlarmsRequest(this);
- }
-
- /// Field number for the "session_id" field.
- public const int SessionIdFieldNumber = 1;
- private string sessionId_ = "";
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
- [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public string SessionId {
- get { return sessionId_; }
- set {
- sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
- }
+ public StreamAlarmsRequest Clone() {
+ return new StreamAlarmsRequest(this);
}
/// Field number for the "client_correlation_id" field.
- public const int ClientCorrelationIdFieldNumber = 2;
+ public const int ClientCorrelationIdFieldNumber = 1;
private string clientCorrelationId_ = "";
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
@@ -26254,11 +26174,11 @@ namespace MxGateway.Contracts.Proto {
}
/// Field number for the "alarm_filter_prefix" field.
- public const int AlarmFilterPrefixFieldNumber = 3;
+ public const int AlarmFilterPrefixFieldNumber = 2;
private string alarmFilterPrefix_ = "";
///
- /// Optional alarm-reference prefix used to scope a partial ConditionRefresh
- /// (e.g. equipment sub-tree). Empty means full refresh.
+ /// Optional alarm-reference prefix scoping the feed to an equipment
+ /// sub-tree. Empty streams every active alarm.
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
@@ -26272,19 +26192,18 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
- return Equals(other as QueryActiveAlarmsRequest);
+ return Equals(other as StreamAlarmsRequest);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public bool Equals(QueryActiveAlarmsRequest other) {
+ public bool Equals(StreamAlarmsRequest other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
- if (SessionId != other.SessionId) return false;
if (ClientCorrelationId != other.ClientCorrelationId) return false;
if (AlarmFilterPrefix != other.AlarmFilterPrefix) return false;
return Equals(_unknownFields, other._unknownFields);
@@ -26294,7 +26213,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
- if (SessionId.Length != 0) hash ^= SessionId.GetHashCode();
if (ClientCorrelationId.Length != 0) hash ^= ClientCorrelationId.GetHashCode();
if (AlarmFilterPrefix.Length != 0) hash ^= AlarmFilterPrefix.GetHashCode();
if (_unknownFields != null) {
@@ -26315,16 +26233,12 @@ namespace MxGateway.Contracts.Proto {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
- output.WriteRawTag(18);
+ output.WriteRawTag(10);
output.WriteString(ClientCorrelationId);
}
if (AlarmFilterPrefix.Length != 0) {
- output.WriteRawTag(26);
+ output.WriteRawTag(18);
output.WriteString(AlarmFilterPrefix);
}
if (_unknownFields != null) {
@@ -26337,16 +26251,12 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
- if (SessionId.Length != 0) {
- output.WriteRawTag(10);
- output.WriteString(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
- output.WriteRawTag(18);
+ output.WriteRawTag(10);
output.WriteString(ClientCorrelationId);
}
if (AlarmFilterPrefix.Length != 0) {
- output.WriteRawTag(26);
+ output.WriteRawTag(18);
output.WriteString(AlarmFilterPrefix);
}
if (_unknownFields != null) {
@@ -26359,9 +26269,6 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
- if (SessionId.Length != 0) {
- size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId);
- }
if (ClientCorrelationId.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(ClientCorrelationId);
}
@@ -26376,13 +26283,10 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public void MergeFrom(QueryActiveAlarmsRequest other) {
+ public void MergeFrom(StreamAlarmsRequest other) {
if (other == null) {
return;
}
- if (other.SessionId.Length != 0) {
- SessionId = other.SessionId;
- }
if (other.ClientCorrelationId.Length != 0) {
ClientCorrelationId = other.ClientCorrelationId;
}
@@ -26409,14 +26313,10 @@ namespace MxGateway.Contracts.Proto {
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
- SessionId = input.ReadString();
- break;
- }
- case 18: {
ClientCorrelationId = input.ReadString();
break;
}
- case 26: {
+ case 18: {
AlarmFilterPrefix = input.ReadString();
break;
}
@@ -26440,14 +26340,10 @@ namespace MxGateway.Contracts.Proto {
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 10: {
- SessionId = input.ReadString();
- break;
- }
- case 18: {
ClientCorrelationId = input.ReadString();
break;
}
- case 26: {
+ case 18: {
AlarmFilterPrefix = input.ReadString();
break;
}
@@ -26458,6 +26354,369 @@ namespace MxGateway.Contracts.Proto {
}
+ ///
+ /// 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.
+ ///
+ [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
+ public sealed partial class AlarmFeedMessage : pb::IMessage
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ , pb::IBufferMessage
+ #endif
+ {
+ private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AlarmFeedMessage());
+ private pb::UnknownFieldSet _unknownFields;
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public static pb::MessageParser Parser { get { return _parser; } }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public static pbr::MessageDescriptor Descriptor {
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[80]; }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ pbr::MessageDescriptor pb::IMessage.Descriptor {
+ get { return Descriptor; }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmFeedMessage() {
+ OnConstruction();
+ }
+
+ partial void OnConstruction();
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmFeedMessage(AlarmFeedMessage other) : this() {
+ switch (other.PayloadCase) {
+ case PayloadOneofCase.ActiveAlarm:
+ ActiveAlarm = other.ActiveAlarm.Clone();
+ break;
+ case PayloadOneofCase.SnapshotComplete:
+ SnapshotComplete = other.SnapshotComplete;
+ break;
+ case PayloadOneofCase.Transition:
+ Transition = other.Transition.Clone();
+ break;
+ }
+
+ _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmFeedMessage Clone() {
+ return new AlarmFeedMessage(this);
+ }
+
+ /// Field number for the "active_alarm" field.
+ public const int ActiveAlarmFieldNumber = 1;
+ ///
+ /// Part of the initial active-alarm snapshot (ConditionRefresh).
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot ActiveAlarm {
+ get { return payloadCase_ == PayloadOneofCase.ActiveAlarm ? (global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot) payload_ : null; }
+ set {
+ payload_ = value;
+ payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.ActiveAlarm;
+ }
+ }
+
+ /// Field number for the "snapshot_complete" field.
+ public const int SnapshotCompleteFieldNumber = 2;
+ ///
+ /// Sentinel: the initial snapshot is fully delivered and `transition`
+ /// messages follow. Always true when present.
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public bool SnapshotComplete {
+ get { return HasSnapshotComplete ? (bool) payload_ : false; }
+ set {
+ payload_ = value;
+ payloadCase_ = PayloadOneofCase.SnapshotComplete;
+ }
+ }
+ /// Gets whether the "snapshot_complete" field is set
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public bool HasSnapshotComplete {
+ get { return payloadCase_ == PayloadOneofCase.SnapshotComplete; }
+ }
+ /// Clears the value of the oneof if it's currently set to "snapshot_complete"
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void ClearSnapshotComplete() {
+ if (HasSnapshotComplete) {
+ ClearPayload();
+ }
+ }
+
+ /// Field number for the "transition" field.
+ public const int TransitionFieldNumber = 3;
+ ///
+ /// A live alarm state change (raise / acknowledge / clear).
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent Transition {
+ get { return payloadCase_ == PayloadOneofCase.Transition ? (global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent) payload_ : null; }
+ set {
+ payload_ = value;
+ payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.Transition;
+ }
+ }
+
+ private object payload_;
+ /// Enum of possible cases for the "payload" oneof.
+ public enum PayloadOneofCase {
+ None = 0,
+ ActiveAlarm = 1,
+ SnapshotComplete = 2,
+ Transition = 3,
+ }
+ private PayloadOneofCase payloadCase_ = PayloadOneofCase.None;
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public PayloadOneofCase PayloadCase {
+ get { return payloadCase_; }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void ClearPayload() {
+ payloadCase_ = PayloadOneofCase.None;
+ payload_ = null;
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override bool Equals(object other) {
+ return Equals(other as AlarmFeedMessage);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public bool Equals(AlarmFeedMessage other) {
+ if (ReferenceEquals(other, null)) {
+ return false;
+ }
+ if (ReferenceEquals(other, this)) {
+ return true;
+ }
+ if (!object.Equals(ActiveAlarm, other.ActiveAlarm)) return false;
+ if (SnapshotComplete != other.SnapshotComplete) return false;
+ if (!object.Equals(Transition, other.Transition)) return false;
+ if (PayloadCase != other.PayloadCase) return false;
+ return Equals(_unknownFields, other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override int GetHashCode() {
+ int hash = 1;
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) hash ^= ActiveAlarm.GetHashCode();
+ if (HasSnapshotComplete) hash ^= SnapshotComplete.GetHashCode();
+ if (payloadCase_ == PayloadOneofCase.Transition) hash ^= Transition.GetHashCode();
+ hash ^= (int) payloadCase_;
+ if (_unknownFields != null) {
+ hash ^= _unknownFields.GetHashCode();
+ }
+ return hash;
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override string ToString() {
+ return pb::JsonFormatter.ToDiagnosticString(this);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void WriteTo(pb::CodedOutputStream output) {
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ output.WriteRawMessage(this);
+ #else
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) {
+ output.WriteRawTag(10);
+ output.WriteMessage(ActiveAlarm);
+ }
+ if (HasSnapshotComplete) {
+ output.WriteRawTag(16);
+ output.WriteBool(SnapshotComplete);
+ }
+ if (payloadCase_ == PayloadOneofCase.Transition) {
+ output.WriteRawTag(26);
+ output.WriteMessage(Transition);
+ }
+ if (_unknownFields != null) {
+ _unknownFields.WriteTo(output);
+ }
+ #endif
+ }
+
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) {
+ output.WriteRawTag(10);
+ output.WriteMessage(ActiveAlarm);
+ }
+ if (HasSnapshotComplete) {
+ output.WriteRawTag(16);
+ output.WriteBool(SnapshotComplete);
+ }
+ if (payloadCase_ == PayloadOneofCase.Transition) {
+ output.WriteRawTag(26);
+ output.WriteMessage(Transition);
+ }
+ if (_unknownFields != null) {
+ _unknownFields.WriteTo(ref output);
+ }
+ }
+ #endif
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public int CalculateSize() {
+ int size = 0;
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) {
+ size += 1 + pb::CodedOutputStream.ComputeMessageSize(ActiveAlarm);
+ }
+ if (HasSnapshotComplete) {
+ size += 1 + 1;
+ }
+ if (payloadCase_ == PayloadOneofCase.Transition) {
+ size += 1 + pb::CodedOutputStream.ComputeMessageSize(Transition);
+ }
+ if (_unknownFields != null) {
+ size += _unknownFields.CalculateSize();
+ }
+ return size;
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void MergeFrom(AlarmFeedMessage other) {
+ if (other == null) {
+ return;
+ }
+ switch (other.PayloadCase) {
+ case PayloadOneofCase.ActiveAlarm:
+ if (ActiveAlarm == null) {
+ ActiveAlarm = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot();
+ }
+ ActiveAlarm.MergeFrom(other.ActiveAlarm);
+ break;
+ case PayloadOneofCase.SnapshotComplete:
+ SnapshotComplete = other.SnapshotComplete;
+ break;
+ case PayloadOneofCase.Transition:
+ if (Transition == null) {
+ Transition = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent();
+ }
+ Transition.MergeFrom(other.Transition);
+ break;
+ }
+
+ _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void MergeFrom(pb::CodedInputStream input) {
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ input.ReadRawMessage(this);
+ #else
+ uint tag;
+ while ((tag = input.ReadTag()) != 0) {
+ if ((tag & 7) == 4) {
+ // Abort on any end group tag.
+ return;
+ }
+ switch(tag) {
+ default:
+ _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
+ break;
+ case 10: {
+ global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot subBuilder = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot();
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) {
+ subBuilder.MergeFrom(ActiveAlarm);
+ }
+ input.ReadMessage(subBuilder);
+ ActiveAlarm = subBuilder;
+ break;
+ }
+ case 16: {
+ SnapshotComplete = input.ReadBool();
+ break;
+ }
+ case 26: {
+ global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent subBuilder = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent();
+ if (payloadCase_ == PayloadOneofCase.Transition) {
+ subBuilder.MergeFrom(Transition);
+ }
+ input.ReadMessage(subBuilder);
+ Transition = subBuilder;
+ break;
+ }
+ }
+ }
+ #endif
+ }
+
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
+ uint tag;
+ while ((tag = input.ReadTag()) != 0) {
+ if ((tag & 7) == 4) {
+ // Abort on any end group tag.
+ return;
+ }
+ switch(tag) {
+ default:
+ _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
+ break;
+ case 10: {
+ global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot subBuilder = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot();
+ if (payloadCase_ == PayloadOneofCase.ActiveAlarm) {
+ subBuilder.MergeFrom(ActiveAlarm);
+ }
+ input.ReadMessage(subBuilder);
+ ActiveAlarm = subBuilder;
+ break;
+ }
+ case 16: {
+ SnapshotComplete = input.ReadBool();
+ break;
+ }
+ case 26: {
+ global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent subBuilder = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent();
+ if (payloadCase_ == PayloadOneofCase.Transition) {
+ subBuilder.MergeFrom(Transition);
+ }
+ input.ReadMessage(subBuilder);
+ Transition = subBuilder;
+ break;
+ }
+ }
+ }
+ }
+ #endif
+
+ }
+
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class MxStatusProxy : pb::IMessage
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -26473,7 +26732,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[80]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[81]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -26904,7 +27163,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[81]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[82]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -27761,7 +28020,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[82]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[83]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -28549,7 +28808,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[83]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[84]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -28738,7 +28997,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[84]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[85]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -28927,7 +29186,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[85]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[86]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29116,7 +29375,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[86]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29305,7 +29564,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29494,7 +29753,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29681,7 +29940,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29868,7 +30127,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -30055,7 +30314,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
+ get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
diff --git a/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs b/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs
index 55606d7..349bfac 100644
--- a/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs
+++ b/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs
@@ -69,9 +69,9 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest.Parser));
+ static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser));
+ static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_AlarmFeedMessage = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AlarmFeedMessage.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method __Method_OpenSession = new grpc::Method(
@@ -114,12 +114,12 @@ namespace MxGateway.Contracts.Proto {
__Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- static readonly grpc::Method __Method_QueryActiveAlarms = new grpc::Method(
+ static readonly grpc::Method __Method_StreamAlarms = new grpc::Method(
grpc::MethodType.ServerStreaming,
__ServiceName,
- "QueryActiveAlarms",
- __Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest,
- __Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot);
+ "StreamAlarms",
+ __Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest,
+ __Marshaller_mxaccess_gateway_v1_AlarmFeedMessage);
/// Service descriptor
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
@@ -161,8 +161,19 @@ namespace MxGateway.Contracts.Proto {
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
+ ///
+ /// 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.
+ ///
+ /// The request received from the client.
+ /// Used for sending responses back to the client.
+ /// The context of the server-side call handler being invoked.
+ /// A task indicating completion of the handler.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- public virtual global::System.Threading.Tasks.Task QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::IServerStreamWriter responseStream, grpc::ServerCallContext context)
+ public virtual global::System.Threading.Tasks.Task StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::IServerStreamWriter responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
@@ -286,15 +297,37 @@ namespace MxGateway.Contracts.Proto {
{
return CallInvoker.AsyncUnaryCall(__Method_AcknowledgeAlarm, null, options, request);
}
+ ///
+ /// 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.
+ ///
+ /// The request to send to the server.
+ /// The initial metadata to send with the call. This parameter is optional.
+ /// An optional deadline for the call. The call will be cancelled if deadline is hit.
+ /// An optional token for canceling the call.
+ /// The call object.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- public virtual grpc::AsyncServerStreamingCall QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
+ public virtual grpc::AsyncServerStreamingCall StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
- return QueryActiveAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken));
+ return StreamAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
+ ///
+ /// 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.
+ ///
+ /// The request to send to the server.
+ /// The options for the call.
+ /// The call object.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
- public virtual grpc::AsyncServerStreamingCall QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::CallOptions options)
+ public virtual grpc::AsyncServerStreamingCall StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::CallOptions options)
{
- return CallInvoker.AsyncServerStreamingCall(__Method_QueryActiveAlarms, null, options, request);
+ return CallInvoker.AsyncServerStreamingCall(__Method_StreamAlarms, null, options, request);
}
/// Creates a new instance of client from given ClientBaseConfiguration.
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
@@ -315,7 +348,7 @@ namespace MxGateway.Contracts.Proto {
.AddMethod(__Method_Invoke, serviceImpl.Invoke)
.AddMethod(__Method_StreamEvents, serviceImpl.StreamEvents)
.AddMethod(__Method_AcknowledgeAlarm, serviceImpl.AcknowledgeAlarm)
- .AddMethod(__Method_QueryActiveAlarms, serviceImpl.QueryActiveAlarms).Build();
+ .AddMethod(__Method_StreamAlarms, serviceImpl.StreamAlarms).Build();
}
/// Register service method with a service binder with or without implementation. Useful when customizing the service binding logic.
@@ -330,7 +363,7 @@ namespace MxGateway.Contracts.Proto {
serviceBinder.AddMethod(__Method_Invoke, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.Invoke));
serviceBinder.AddMethod(__Method_StreamEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.StreamEvents));
serviceBinder.AddMethod(__Method_AcknowledgeAlarm, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.AcknowledgeAlarm));
- serviceBinder.AddMethod(__Method_QueryActiveAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.QueryActiveAlarms));
+ serviceBinder.AddMethod(__Method_StreamAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.StreamAlarms));
}
}
diff --git a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto
index 9a74078..4496acd 100644
--- a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto
+++ b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto
@@ -11,8 +11,7 @@ import "google/protobuf/timestamp.proto";
// 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.
service MxAccessGateway {
@@ -21,7 +20,12 @@ service MxAccessGateway {
rpc Invoke(MxCommandRequest) returns (MxCommandReply);
rpc StreamEvents(StreamEventsRequest) returns (stream MxEvent);
rpc AcknowledgeAlarm(AcknowledgeAlarmRequest) returns (AcknowledgeAlarmReply);
- rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns (stream ActiveAlarmSnapshot);
+ // Session-less central alarm feed. The stream opens with the current
+ // active-alarm snapshot (one `active_alarm` per alarm), then a single
+ // `snapshot_complete`, then a `transition` for every subsequent change.
+ // Served by the gateway's always-on alarm monitor; any number of clients
+ // fan out from the single monitor without opening a worker session.
+ rpc StreamAlarms(StreamAlarmsRequest) returns (stream AlarmFeedMessage);
}
message OpenSessionRequest {
@@ -785,7 +789,10 @@ enum AlarmConditionState {
}
message AcknowledgeAlarmRequest {
- string session_id = 1;
+ // Retired: acknowledgement is session-less — it routes to the gateway's
+ // central alarm monitor, not a client worker session.
+ reserved 1;
+ reserved "session_id";
string client_correlation_id = 2;
// Fully-qualified alarm reference matching OnAlarmTransitionEvent.alarm_full_reference.
string alarm_full_reference = 3;
@@ -797,7 +804,9 @@ message AcknowledgeAlarmRequest {
}
message AcknowledgeAlarmReply {
- string session_id = 1;
+ // Retired: see AcknowledgeAlarmRequest — acknowledgement is session-less.
+ reserved 1;
+ reserved "session_id";
string correlation_id = 2;
ProtocolStatus protocol_status = 3;
// Native ack return code echoed from the worker. The worker carries the
@@ -816,12 +825,27 @@ message AcknowledgeAlarmReply {
string diagnostic_message = 6;
}
-message QueryActiveAlarmsRequest {
- string session_id = 1;
- string client_correlation_id = 2;
- // Optional alarm-reference prefix used to scope a partial ConditionRefresh
- // (e.g. equipment sub-tree). Empty means full refresh.
- string alarm_filter_prefix = 3;
+// Request to attach to the gateway's central alarm feed (StreamAlarms).
+message StreamAlarmsRequest {
+ string client_correlation_id = 1;
+ // Optional alarm-reference prefix scoping the feed to an equipment
+ // sub-tree. Empty streams every active alarm.
+ string alarm_filter_prefix = 2;
+}
+
+// One message on the StreamAlarms feed. The stream opens with one
+// `active_alarm` per currently-active alarm, then a single
+// `snapshot_complete`, then a `transition` for every subsequent change.
+message AlarmFeedMessage {
+ oneof payload {
+ // Part of the initial active-alarm snapshot (ConditionRefresh).
+ ActiveAlarmSnapshot active_alarm = 1;
+ // Sentinel: the initial snapshot is fully delivered and `transition`
+ // messages follow. Always true when present.
+ bool snapshot_complete = 2;
+ // A live alarm state change (raise / acknowledge / clear).
+ OnAlarmTransitionEvent transition = 3;
+ }
}
message MxStatusProxy {
diff --git a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
index b0b1311..686dd0a 100644
--- a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
+++ b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
@@ -1084,7 +1084,11 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
mapper,
eventStreamService,
_metrics,
- _loggerFactory.CreateLogger());
+ _loggerFactory.CreateLogger(),
+ new MxGateway.Server.Alarms.GatewayAlarmMonitor(
+ sessionManager,
+ options,
+ _loggerFactory.CreateLogger()));
}
///
diff --git a/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs b/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs
new file mode 100644
index 0000000..b88b5c6
--- /dev/null
+++ b/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+namespace MxGateway.Server.Alarms;
+
+/// Service-collection wiring for the gateway's central alarm monitor.
+public static class AlarmsServiceCollectionExtensions
+{
+ ///
+ /// Registers the always-on as both
+ /// the singleton and a hosted
+ /// service, so it starts with the gateway host and is shared by the
+ /// gRPC alarm surface and the dashboard.
+ ///
+ /// Service collection to register services in.
+ /// The service collection for chaining.
+ public static IServiceCollection AddGatewayAlarms(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddHostedService(provider => provider.GetRequiredService());
+
+ return services;
+ }
+}
diff --git a/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
new file mode 100644
index 0000000..fa334b4
--- /dev/null
+++ b/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
@@ -0,0 +1,693 @@
+using System.Threading.Channels;
+using Microsoft.Extensions.Options;
+using MxGateway.Contracts.Proto;
+using MxGateway.Server.Configuration;
+using MxGateway.Server.Sessions;
+
+namespace MxGateway.Server.Alarms;
+
+///
+/// The gateway's always-on alarm monitor and broker. It owns one
+/// gateway-managed worker session dedicated to alarms, keeps an in-process
+/// cache of the active-alarm set fed by that session's transition events
+/// (reconciled periodically against the worker's snapshot), and fans the
+/// feed out to any number of subscribers.
+/// The session is re-opened transparently if the worker faults.
+///
+public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmService
+{
+ private const string MonitorClientName = "gateway-alarm-monitor";
+ private const string BackendName = "Galaxy";
+ private const int SubscriberQueueCapacity = 2048;
+ private static readonly TimeSpan RestartBackoff = TimeSpan.FromSeconds(5);
+ private static readonly TimeSpan StartupGrace = TimeSpan.FromSeconds(2);
+
+ private readonly ISessionManager _sessionManager;
+ private readonly AlarmsOptions _options;
+ private readonly ILogger _logger;
+
+ private readonly object _sync = new();
+ private readonly Dictionary _alarms = new(StringComparer.Ordinal);
+ private readonly List _subscribers = [];
+
+ private volatile GatewayAlarmMonitorState _state = GatewayAlarmMonitorState.Disabled;
+ private volatile string? _lastError;
+ private GatewaySession? _session;
+
+ /// Initializes the gateway alarm monitor.
+ /// Gateway session manager.
+ /// Gateway options carrying the alarm configuration.
+ /// Diagnostic logger.
+ public GatewayAlarmMonitor(
+ ISessionManager sessionManager,
+ IOptions options,
+ ILogger logger)
+ {
+ _sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
+ _options = (options ?? throw new ArgumentNullException(nameof(options))).Value.Alarms;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public GatewayAlarmMonitorState State => _state;
+
+ ///
+ public string? LastError => _lastError;
+
+ ///
+ public int? WorkerProcessId
+ {
+ get { lock (_sync) { return _session?.WorkerProcessId; } }
+ }
+
+ ///
+ public IReadOnlyList CurrentAlarms
+ {
+ get
+ {
+ lock (_sync)
+ {
+ return _alarms.Values.Select(alarm => alarm.Clone()).ToArray();
+ }
+ }
+ }
+
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ if (!_options.Enabled)
+ {
+ _state = GatewayAlarmMonitorState.Disabled;
+ _logger.LogInformation("Gateway alarm monitor disabled (MxGateway:Alarms:Enabled is false).");
+ return;
+ }
+
+ string subscription = ResolveSubscription();
+ if (string.IsNullOrWhiteSpace(subscription))
+ {
+ _state = GatewayAlarmMonitorState.Faulted;
+ _lastError = "MxGateway:Alarms is enabled but no SubscriptionExpression / DefaultArea is configured.";
+ _logger.LogError("{Diagnostic}", _lastError);
+ return;
+ }
+
+ // Brief grace so worker-process launching and startup orphan cleanup
+ // settle before the monitor opens its own session.
+ try
+ {
+ await Task.Delay(StartupGrace, stoppingToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await RunMonitorAsync(subscription, stoppingToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ break;
+ }
+ catch (Exception exception)
+ {
+ _state = GatewayAlarmMonitorState.Faulted;
+ _lastError = exception.Message;
+ _logger.LogWarning(
+ exception,
+ "Gateway alarm monitor lifecycle faulted; restarting in {Backoff}.",
+ RestartBackoff);
+ try
+ {
+ await Task.Delay(RestartBackoff, stoppingToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+ }
+
+ _state = GatewayAlarmMonitorState.Disabled;
+ }
+
+ // One monitoring lifecycle: open a session, subscribe alarms, reconcile,
+ // then consume transition events until the session ends or is cancelled.
+ private async Task RunMonitorAsync(string subscription, CancellationToken stoppingToken)
+ {
+ _state = GatewayAlarmMonitorState.Starting;
+ GatewaySession session = await _sessionManager.OpenSessionAsync(
+ new SessionOpenRequest(BackendName, MonitorClientName, Guid.NewGuid().ToString("N"), CommandTimeout: null),
+ MonitorClientName,
+ stoppingToken)
+ .ConfigureAwait(false);
+ lock (_sync) { _session = session; }
+
+ try
+ {
+ await SubscribeAlarmsAsync(session.SessionId, subscription, stoppingToken).ConfigureAwait(false);
+ await ReconcileAsync(session.SessionId, stoppingToken).ConfigureAwait(false);
+
+ _state = GatewayAlarmMonitorState.Monitoring;
+ _lastError = null;
+ _logger.LogInformation(
+ "Gateway alarm monitor active on {Subscription} (session {SessionId}, worker pid {WorkerPid}).",
+ subscription,
+ session.SessionId,
+ session.WorkerProcessId);
+
+ using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
+ Task reconcileLoop = ReconcileLoopAsync(session.SessionId, linked.Token);
+ try
+ {
+ await foreach (WorkerEvent workerEvent in _sessionManager
+ .ReadEventsAsync(session.SessionId, linked.Token)
+ .ConfigureAwait(false))
+ {
+ MxEvent? mxEvent = workerEvent.Event;
+ if (mxEvent is { BodyCase: MxEvent.BodyOneofCase.OnAlarmTransition }
+ && mxEvent.OnAlarmTransition is not null)
+ {
+ ApplyTransition(mxEvent.OnAlarmTransition);
+ }
+ }
+ }
+ finally
+ {
+ await linked.CancelAsync().ConfigureAwait(false);
+ try
+ {
+ await reconcileLoop.ConfigureAwait(false);
+ }
+ catch
+ {
+ // Reconcile-loop teardown errors are not actionable here.
+ }
+ }
+
+ // The event stream ended without cancellation — the worker session
+ // closed or faulted. Surface it so the supervisor loop restarts.
+ throw new InvalidOperationException("Alarm monitor worker event stream ended.");
+ }
+ finally
+ {
+ lock (_sync) { _session = null; }
+ ClearCache();
+ try
+ {
+ await _sessionManager.CloseSessionAsync(session.SessionId, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogDebug(exception, "Closing alarm monitor session {SessionId} failed.", session.SessionId);
+ }
+ }
+ }
+
+ private async Task SubscribeAlarmsAsync(string sessionId, string subscription, CancellationToken cancellationToken)
+ {
+ WorkerCommandReply reply = await _sessionManager.InvokeAsync(
+ sessionId,
+ new WorkerCommand
+ {
+ Command = new MxCommand
+ {
+ Kind = MxCommandKind.SubscribeAlarms,
+ SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = subscription },
+ },
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code;
+ if (code != ProtocolStatusCode.Ok)
+ {
+ string diagnostic = reply.Reply?.DiagnosticMessage
+ ?? reply.Reply?.ProtocolStatus?.Message
+ ?? $"status {code}";
+ throw new InvalidOperationException($"Worker rejected SubscribeAlarms: {diagnostic}");
+ }
+ }
+
+ private async Task ReconcileLoopAsync(string sessionId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ int seconds = Math.Max(5, _options.ReconcileIntervalSeconds);
+ using PeriodicTimer timer = new(TimeSpan.FromSeconds(seconds));
+ while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
+ {
+ try
+ {
+ await ReconcileAsync(sessionId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception exception)
+ {
+ _logger.LogDebug(exception, "Alarm reconcile pass failed; keeping the current cache.");
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ private async Task ReconcileAsync(string sessionId, CancellationToken cancellationToken)
+ {
+ WorkerCommandReply reply = await _sessionManager.InvokeAsync(
+ sessionId,
+ new WorkerCommand
+ {
+ Command = new MxCommand
+ {
+ Kind = MxCommandKind.QueryActiveAlarms,
+ QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand { AlarmFilterPrefix = string.Empty },
+ },
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (reply.Reply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok)
+ {
+ return;
+ }
+
+ QueryActiveAlarmsReplyPayload? payload = reply.Reply.QueryActiveAlarms;
+ if (payload is not null)
+ {
+ ApplyReconcile(payload.Snapshots);
+ }
+ }
+
+ // Applies a live transition to the cache and broadcasts it to subscribers.
+ private void ApplyTransition(OnAlarmTransitionEvent transition)
+ {
+ string reference = transition.AlarmFullReference ?? string.Empty;
+ if (reference.Length == 0)
+ {
+ return;
+ }
+
+ lock (_sync)
+ {
+ if (transition.TransitionKind == AlarmTransitionKind.Clear)
+ {
+ _alarms.Remove(reference);
+ }
+ else
+ {
+ _alarms[reference] = SnapshotFromTransition(transition);
+ }
+
+ Broadcast(new AlarmFeedMessage { Transition = transition }, reference);
+ }
+ }
+
+ // Replaces the cache with the worker's authoritative snapshot, broadcasting
+ // a synthetic transition for any alarm the live stream missed.
+ private void ApplyReconcile(IEnumerable snapshots)
+ {
+ Dictionary next = new(StringComparer.Ordinal);
+ foreach (ActiveAlarmSnapshot snapshot in snapshots)
+ {
+ if (!string.IsNullOrEmpty(snapshot.AlarmFullReference))
+ {
+ next[snapshot.AlarmFullReference] = snapshot;
+ }
+ }
+
+ lock (_sync)
+ {
+ foreach (KeyValuePair existing in _alarms)
+ {
+ if (!next.ContainsKey(existing.Key))
+ {
+ Broadcast(
+ new AlarmFeedMessage { Transition = TransitionFromSnapshot(existing.Value, AlarmTransitionKind.Clear) },
+ existing.Key);
+ }
+ }
+
+ foreach (KeyValuePair incoming in next)
+ {
+ if (!_alarms.ContainsKey(incoming.Key))
+ {
+ Broadcast(
+ new AlarmFeedMessage { Transition = TransitionFromSnapshot(incoming.Value, AlarmTransitionKind.Raise) },
+ incoming.Key);
+ }
+ }
+
+ _alarms.Clear();
+ foreach (KeyValuePair incoming in next)
+ {
+ _alarms[incoming.Key] = incoming.Value;
+ }
+ }
+ }
+
+ // Caller holds _sync. Pushes a feed message to every matching subscriber;
+ // a subscriber that has fallen behind is completed with an error and dropped.
+ private void Broadcast(AlarmFeedMessage message, string reference)
+ {
+ for (int index = _subscribers.Count - 1; index >= 0; index--)
+ {
+ Subscriber subscriber = _subscribers[index];
+ if (!subscriber.Matches(reference))
+ {
+ continue;
+ }
+
+ if (!subscriber.Channel.Writer.TryWrite(message))
+ {
+ subscriber.Channel.Writer.TryComplete(new InvalidOperationException(
+ "Alarm feed subscriber fell behind and was dropped; reconnect to re-snapshot."));
+ _subscribers.RemoveAt(index);
+ }
+ }
+ }
+
+ private void ClearCache()
+ {
+ lock (_sync)
+ {
+ _alarms.Clear();
+ }
+ }
+
+ ///
+ public async IAsyncEnumerable StreamAsync(
+ string? alarmFilterPrefix,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ string prefix = alarmFilterPrefix ?? string.Empty;
+ Channel channel = Channel.CreateBounded(
+ new BoundedChannelOptions(SubscriberQueueCapacity)
+ {
+ FullMode = BoundedChannelFullMode.Wait,
+ SingleReader = true,
+ SingleWriter = false,
+ });
+ Subscriber subscriber = new(channel, prefix);
+
+ ActiveAlarmSnapshot[] snapshot;
+ lock (_sync)
+ {
+ // Register before snapshotting under the same lock so no transition
+ // can slip between the snapshot and the live stream.
+ _subscribers.Add(subscriber);
+ snapshot = _alarms.Values
+ .Where(alarm => prefix.Length == 0
+ || alarm.AlarmFullReference.StartsWith(prefix, StringComparison.Ordinal))
+ .Select(alarm => alarm.Clone())
+ .ToArray();
+ }
+
+ try
+ {
+ foreach (ActiveAlarmSnapshot alarm in snapshot)
+ {
+ yield return new AlarmFeedMessage { ActiveAlarm = alarm };
+ }
+
+ yield return new AlarmFeedMessage { SnapshotComplete = true };
+
+ await foreach (AlarmFeedMessage message in channel.Reader
+ .ReadAllAsync(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ yield return message;
+ }
+ }
+ finally
+ {
+ lock (_sync) { _subscribers.Remove(subscriber); }
+ channel.Writer.TryComplete();
+ }
+ }
+
+ ///
+ public async Task AcknowledgeAsync(
+ AcknowledgeAlarmRequest request,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ string? sessionId;
+ lock (_sync) { sessionId = _session?.SessionId; }
+ if (sessionId is null || _state != GatewayAlarmMonitorState.Monitoring)
+ {
+ return new AcknowledgeAlarmReply
+ {
+ CorrelationId = request.ClientCorrelationId,
+ ProtocolStatus = new ProtocolStatus
+ {
+ Code = ProtocolStatusCode.WorkerUnavailable,
+ Message = "Gateway alarm monitor is not currently active.",
+ },
+ DiagnosticMessage = _lastError ?? "Alarm monitor is not running.",
+ };
+ }
+
+ MxCommand? command = BuildAcknowledgeCommand(request, out string? parseError);
+ if (command is null)
+ {
+ return new AcknowledgeAlarmReply
+ {
+ CorrelationId = request.ClientCorrelationId,
+ ProtocolStatus = new ProtocolStatus
+ {
+ Code = ProtocolStatusCode.InvalidRequest,
+ Message = parseError ?? "Invalid acknowledge request.",
+ },
+ DiagnosticMessage = parseError ?? "Invalid acknowledge request.",
+ };
+ }
+
+ WorkerCommandReply workerReply = await _sessionManager
+ .InvokeAsync(sessionId, new WorkerCommand { Command = command }, cancellationToken)
+ .ConfigureAwait(false);
+
+ MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply
+ {
+ ProtocolStatus = new ProtocolStatus
+ {
+ Code = ProtocolStatusCode.ProtocolViolation,
+ Message = "Worker reply did not include an MxCommandReply.",
+ },
+ };
+
+ AcknowledgeAlarmReply reply = new()
+ {
+ CorrelationId = request.ClientCorrelationId,
+ ProtocolStatus = mxReply.ProtocolStatus ?? new ProtocolStatus { Code = ProtocolStatusCode.Ok },
+ DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty,
+ };
+ if (mxReply.HasHresult)
+ {
+ reply.Hresult = mxReply.Hresult;
+ }
+
+ return reply;
+ }
+
+ private string ResolveSubscription()
+ {
+ if (!string.IsNullOrWhiteSpace(_options.SubscriptionExpression))
+ {
+ return _options.SubscriptionExpression;
+ }
+
+ if (!string.IsNullOrWhiteSpace(_options.DefaultArea))
+ {
+ return $@"\\{Environment.MachineName}\Galaxy!{_options.DefaultArea}";
+ }
+
+ return string.Empty;
+ }
+
+ private static MxCommand? BuildAcknowledgeCommand(AcknowledgeAlarmRequest request, out string? parseError)
+ {
+ parseError = null;
+ if (string.IsNullOrWhiteSpace(request.AlarmFullReference))
+ {
+ parseError = "alarm_full_reference is required.";
+ return null;
+ }
+
+ string comment = request.Comment ?? string.Empty;
+ string operatorUser = request.OperatorUser ?? string.Empty;
+
+ if (Guid.TryParse(request.AlarmFullReference, out Guid guid))
+ {
+ return new MxCommand
+ {
+ Kind = MxCommandKind.AcknowledgeAlarm,
+ AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
+ {
+ AlarmGuid = guid.ToString(),
+ Comment = comment,
+ OperatorUser = operatorUser,
+ OperatorNode = string.Empty,
+ OperatorDomain = string.Empty,
+ OperatorFullName = string.Empty,
+ },
+ };
+ }
+
+ if (TryParseAlarmReference(request.AlarmFullReference, out string provider, out string group, out string alarm))
+ {
+ return new MxCommand
+ {
+ Kind = MxCommandKind.AcknowledgeAlarmByName,
+ AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
+ {
+ AlarmName = alarm,
+ ProviderName = provider,
+ GroupName = group,
+ Comment = comment,
+ OperatorUser = operatorUser,
+ OperatorNode = string.Empty,
+ OperatorDomain = string.Empty,
+ OperatorFullName = string.Empty,
+ },
+ };
+ }
+
+ parseError = "alarm_full_reference must be a canonical GUID or 'Provider!Group.Tag' format.";
+ return null;
+ }
+
+ ///
+ /// Parses an alarm reference of the form Provider!Group.Tag: the
+ /// first ! splits provider from Group.Tag; the first
+ /// . after the ! splits group from tag.
+ ///
+ /// The full alarm reference.
+ /// The parsed provider.
+ /// The parsed group/area.
+ /// The parsed tag/alarm name.
+ /// true on a well-formed reference; otherwise false.
+ public static bool TryParseAlarmReference(
+ string? reference,
+ out string providerName,
+ out string groupName,
+ out string alarmName)
+ {
+ providerName = string.Empty;
+ groupName = string.Empty;
+ alarmName = string.Empty;
+ if (string.IsNullOrWhiteSpace(reference))
+ {
+ return false;
+ }
+
+ int bang = reference!.IndexOf('!', StringComparison.Ordinal);
+ if (bang <= 0 || bang == reference.Length - 1)
+ {
+ return false;
+ }
+
+ string left = reference[..bang];
+ string right = reference[(bang + 1)..];
+ int dot = right.IndexOf('.', StringComparison.Ordinal);
+ if (dot <= 0 || dot == right.Length - 1)
+ {
+ return false;
+ }
+
+ providerName = left;
+ groupName = right[..dot];
+ alarmName = right[(dot + 1)..];
+ return true;
+ }
+
+ private static ActiveAlarmSnapshot SnapshotFromTransition(OnAlarmTransitionEvent transition)
+ {
+ ActiveAlarmSnapshot snapshot = new()
+ {
+ AlarmFullReference = transition.AlarmFullReference,
+ SourceObjectReference = transition.SourceObjectReference,
+ AlarmTypeName = transition.AlarmTypeName,
+ Severity = transition.Severity,
+ CurrentState = transition.TransitionKind == AlarmTransitionKind.Acknowledge
+ ? AlarmConditionState.ActiveAcked
+ : AlarmConditionState.Active,
+ Category = transition.Category,
+ Description = transition.Description,
+ OperatorUser = transition.OperatorUser,
+ OperatorComment = transition.OperatorComment,
+ };
+ if (transition.OriginalRaiseTimestamp is not null)
+ {
+ snapshot.OriginalRaiseTimestamp = transition.OriginalRaiseTimestamp;
+ }
+ if (transition.TransitionTimestamp is not null)
+ {
+ snapshot.LastTransitionTimestamp = transition.TransitionTimestamp;
+ }
+ if (transition.CurrentValue is not null)
+ {
+ snapshot.CurrentValue = transition.CurrentValue;
+ }
+ if (transition.LimitValue is not null)
+ {
+ snapshot.LimitValue = transition.LimitValue;
+ }
+
+ return snapshot;
+ }
+
+ private static OnAlarmTransitionEvent TransitionFromSnapshot(
+ ActiveAlarmSnapshot snapshot,
+ AlarmTransitionKind kind)
+ {
+ OnAlarmTransitionEvent transition = new()
+ {
+ AlarmFullReference = snapshot.AlarmFullReference,
+ SourceObjectReference = snapshot.SourceObjectReference,
+ AlarmTypeName = snapshot.AlarmTypeName,
+ TransitionKind = kind,
+ Severity = snapshot.Severity,
+ Category = snapshot.Category,
+ Description = snapshot.Description,
+ OperatorUser = snapshot.OperatorUser,
+ OperatorComment = snapshot.OperatorComment,
+ };
+ if (snapshot.OriginalRaiseTimestamp is not null)
+ {
+ transition.OriginalRaiseTimestamp = snapshot.OriginalRaiseTimestamp;
+ }
+ if (snapshot.LastTransitionTimestamp is not null)
+ {
+ transition.TransitionTimestamp = snapshot.LastTransitionTimestamp;
+ }
+ if (snapshot.CurrentValue is not null)
+ {
+ transition.CurrentValue = snapshot.CurrentValue;
+ }
+ if (snapshot.LimitValue is not null)
+ {
+ transition.LimitValue = snapshot.LimitValue;
+ }
+
+ return transition;
+ }
+
+ private sealed class Subscriber(Channel channel, string prefix)
+ {
+ public Channel Channel { get; } = channel;
+
+ public bool Matches(string reference)
+ {
+ return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs b/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs
new file mode 100644
index 0000000..281d61b
--- /dev/null
+++ b/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs
@@ -0,0 +1,63 @@
+using MxGateway.Contracts.Proto;
+
+namespace MxGateway.Server.Alarms;
+
+/// Lifecycle state of the gateway's central alarm monitor.
+public enum GatewayAlarmMonitorState
+{
+ /// Alarm monitoring is switched off (MxGateway:Alarms:Enabled is false).
+ Disabled,
+
+ /// The monitor is opening or re-opening its worker session.
+ Starting,
+
+ /// The monitor is connected and tracking the active-alarm set.
+ Monitoring,
+
+ /// The monitor's last lifecycle attempt failed; a restart is pending.
+ Faulted,
+}
+
+///
+/// The gateway's always-on alarm broker. A single gateway-owned worker
+/// session monitors the AVEVA alarm provider; this service caches the
+/// current active-alarm set and fans it out to any number of clients —
+/// no client needs to open its own worker session to see alarms.
+///
+public interface IGatewayAlarmService
+{
+ /// Current monitor lifecycle state.
+ GatewayAlarmMonitorState State { get; }
+
+ /// Diagnostic message from the most recent fault, or null.
+ string? LastError { get; }
+
+ /// Process id of the worker backing the monitor, when one is attached.
+ int? WorkerProcessId { get; }
+
+ /// A point-in-time copy of the current active-alarm set.
+ IReadOnlyList CurrentAlarms { get; }
+
+ ///
+ /// Attaches to the central alarm feed. The returned stream yields one
+ /// per currently-active alarm, then a
+ /// single snapshot_complete sentinel, then a transition
+ /// for every subsequent change.
+ ///
+ /// Optional alarm-reference prefix scoping the feed.
+ /// Token that ends the subscription.
+ IAsyncEnumerable StreamAsync(
+ string? alarmFilterPrefix,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Acknowledges an alarm through the monitor's worker session. Never
+ /// throws — transport and monitor-state failures surface in the
+ /// reply's .
+ ///
+ /// The acknowledge request.
+ /// Token to cancel the call.
+ Task AcknowledgeAsync(
+ AcknowledgeAlarmRequest request,
+ CancellationToken cancellationToken);
+}
diff --git a/src/MxGateway.Server/Configuration/AlarmsOptions.cs b/src/MxGateway.Server/Configuration/AlarmsOptions.cs
index 2722fac..dd47d78 100644
--- a/src/MxGateway.Server/Configuration/AlarmsOptions.cs
+++ b/src/MxGateway.Server/Configuration/AlarmsOptions.cs
@@ -1,32 +1,32 @@
namespace MxGateway.Server.Configuration;
///
-/// Per-gateway alarm-subsystem configuration. Drives the auto-subscribe
-/// hook in : when
-/// is true and a session reaches Ready, the
-/// manager issues a SubscribeAlarmsCommand to the worker with
-/// the configured .
+/// Configuration for the gateway's always-on central alarm monitor
+/// (). When
+/// is 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.
///
///
-/// Defaults preserve current behaviour (alarms disabled). Operators
-/// opt in by setting MxGateway:Alarms:Enabled = true and
-/// supplying a canonical
-/// \\<machine>\Galaxy!<area> subscription
-/// expression. The literal "Galaxy" provider is correct regardless of
-/// the configured Galaxy database name (the wnwrap consumer doesn't
-/// accept the database name as the provider).
+/// Defaults preserve current behaviour (alarm monitoring disabled).
+/// Operators opt in by setting MxGateway:Alarms:Enabled = true and
+/// supplying a canonical \\<machine>\Galaxy!<area>
+/// subscription expression. The literal "Galaxy" provider is correct
+/// regardless of the configured Galaxy database name (the wnwrap consumer
+/// does not accept the database name as the provider).
///
public sealed class AlarmsOptions
{
- /// Gate the auto-subscribe hook on session open. Default false.
+ /// Gate the gateway's always-on central alarm monitor. Default false.
public bool Enabled { get; init; }
///
- /// AVEVA alarm-subscription expression. When empty and
- /// is true, the gateway falls back to
- /// \\$(MachineName)\Galaxy!$(DefaultArea) if
- /// is set; otherwise the session open
- /// fails with a configuration diagnostic.
+ /// AVEVA alarm-subscription expression the monitor subscribes on
+ /// startup. When empty and is true, the gateway
+ /// falls back to \\$(MachineName)\Galaxy!$(DefaultArea) if
+ /// is set; otherwise the monitor faults with
+ /// a configuration diagnostic.
///
public string SubscriptionExpression { get; init; } = string.Empty;
@@ -39,10 +39,10 @@ public sealed class AlarmsOptions
public string DefaultArea { get; init; } = string.Empty;
///
- /// If true, an auto-subscribe failure faults the session. If false
- /// (default), the failure is logged and the session remains Ready —
- /// alarm-side commands return "not subscribed" but data subscriptions
- /// work normally.
+ /// How often the monitor reconciles its in-process alarm cache against
+ /// the worker's authoritative active-alarm snapshot, catching any
+ /// transitions the live poll-and-diff feed missed. Default 30 seconds;
+ /// the monitor floors it at 5 seconds.
///
- public bool RequireSubscribeOnOpen { get; init; }
+ public int ReconcileIntervalSeconds { get; init; } = 30;
}
diff --git a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs
index 3421607..77a0288 100644
--- a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs
+++ b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs
@@ -236,11 +236,10 @@ public sealed class GatewayOptionsValidator : IValidateOptions
return;
}
- // When the alarm auto-subscribe hook is enabled, the gateway needs either a
- // canonical SubscriptionExpression or a DefaultArea to compose one from. Both
- // empty is the configuration mistake SessionManager.TryAutoSubscribeAlarmsAsync
- // currently surfaces per-session — pulling it up to startup validation makes
- // the misconfiguration fail-fast at boot, in line with every other section.
+ // When the central alarm monitor is enabled, it needs either a canonical
+ // SubscriptionExpression or a DefaultArea to compose one from. Validating
+ // it at startup makes the misconfiguration fail-fast at boot, in line
+ // with every other section.
if (string.IsNullOrWhiteSpace(options.SubscriptionExpression)
&& string.IsNullOrWhiteSpace(options.DefaultArea))
{
diff --git a/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs b/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs
index 6efe838..a43e773 100644
--- a/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs
+++ b/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs
@@ -1,4 +1,5 @@
using MxGateway.Contracts.Proto;
+using MxGateway.Server.Alarms;
using MxGateway.Server.Sessions;
namespace MxGateway.Server.Dashboard;
@@ -17,7 +18,7 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5);
private readonly ISessionManager _sessionManager;
- private readonly IAlarmRpcDispatcher _alarmDispatcher;
+ private readonly IGatewayAlarmService _alarmService;
private readonly ILogger _logger;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly HashSet _subscribed = new(StringComparer.OrdinalIgnoreCase);
@@ -28,15 +29,15 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
/// Initializes the live-data service.
/// Gateway session manager.
- /// Active-alarm query dispatcher.
+ /// Gateway central alarm service.
/// Diagnostic logger.
public DashboardLiveDataService(
ISessionManager sessionManager,
- IAlarmRpcDispatcher alarmDispatcher,
+ IGatewayAlarmService alarmService,
ILogger logger)
{
_sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
- _alarmDispatcher = alarmDispatcher ?? throw new ArgumentNullException(nameof(alarmDispatcher));
+ _alarmService = alarmService ?? throw new ArgumentNullException(nameof(alarmService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -90,39 +91,20 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
}
///
- public async Task QueryAlarmsAsync(CancellationToken cancellationToken)
+ public Task QueryAlarmsAsync(CancellationToken cancellationToken)
{
- await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- (GatewaySession session, _) = await EnsureReadyAsync(cancellationToken).ConfigureAwait(false);
+ // Alarms come from the gateway's always-on central monitor; the
+ // dashboard reads its in-process cache directly — no session needed.
+ DashboardActiveAlarm[] alarms = _alarmService.CurrentAlarms
+ .Select(DashboardActiveAlarm.FromSnapshot)
+ .ToArray();
- QueryActiveAlarmsRequest request = new()
- {
- SessionId = session.SessionId,
- ClientCorrelationId = Guid.NewGuid().ToString("N"),
- };
+ string? error = _alarmService.State is GatewayAlarmMonitorState.Monitoring
+ or GatewayAlarmMonitorState.Disabled
+ ? null
+ : _alarmService.LastError ?? $"Alarm monitor is {_alarmService.State}.";
- List alarms = [];
- await foreach (ActiveAlarmSnapshot snapshot in _alarmDispatcher
- .QueryActiveAlarmsAsync(request, cancellationToken)
- .ConfigureAwait(false))
- {
- alarms.Add(DashboardActiveAlarm.FromSnapshot(snapshot));
- }
-
- return new DashboardAlarmQueryResult(alarms, null, session.WorkerProcessId);
- }
- catch (Exception exception) when (exception is not OperationCanceledException)
- {
- InvalidateSession();
- _logger.LogWarning(exception, "Dashboard alarm query failed; the dashboard session will be re-opened.");
- return new DashboardAlarmQueryResult([], exception.Message, null);
- }
- finally
- {
- _gate.Release();
- }
+ return Task.FromResult(new DashboardAlarmQueryResult(alarms, error, _alarmService.WorkerProcessId));
}
// Returns a Ready session + its Register server handle, opening a fresh
diff --git a/src/MxGateway.Server/GatewayApplication.cs b/src/MxGateway.Server/GatewayApplication.cs
index bb19b90..c450065 100644
--- a/src/MxGateway.Server/GatewayApplication.cs
+++ b/src/MxGateway.Server/GatewayApplication.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using MxGateway.Contracts;
+using MxGateway.Server.Alarms;
using MxGateway.Server.Configuration;
using MxGateway.Server.Dashboard;
using MxGateway.Server.Diagnostics;
@@ -64,6 +65,7 @@ public static class GatewayApplication
builder.Services.AddSingleton();
builder.Services.AddWorkerProcessLauncher();
builder.Services.AddGatewaySessions();
+ builder.Services.AddGatewayAlarms();
builder.Services.AddGatewayDashboard();
builder.Services.AddGalaxyRepository();
diff --git a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
index c59bde7..e143881 100644
--- a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
+++ b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs
@@ -3,6 +3,7 @@ using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts;
using MxGateway.Contracts.Proto;
+using MxGateway.Server.Alarms;
using MxGateway.Server.Metrics;
using MxGateway.Server.Security.Authentication;
using MxGateway.Server.Security.Authorization;
@@ -21,9 +22,8 @@ public sealed class MxAccessGatewayService(
IEventStreamService eventStreamService,
GatewayMetrics metrics,
ILogger logger,
- IAlarmRpcDispatcher? alarmRpcDispatcher = null) : MxAccessGateway.MxAccessGatewayBase
+ IGatewayAlarmService alarmService) : MxAccessGateway.MxAccessGatewayBase
{
- private readonly IAlarmRpcDispatcher alarmRpcDispatcher = alarmRpcDispatcher ?? new NotWiredAlarmRpcDispatcher();
///
public override async Task OpenSession(
OpenSessionRequest request,
@@ -163,14 +163,13 @@ public sealed class MxAccessGatewayService(
///
///
- /// Surfaces the public AcknowledgeAlarm RPC. The gateway validates the request,
- /// resolves the session, and delegates to the registered
- /// . DI binds the production
- /// , which routes
- /// the ack through the worker pipe IPC: an alarm_full_reference that parses
- /// as a canonical GUID forwards to AcknowledgeAlarmCommand; a
- /// Provider!Group.Tag reference forwards to AcknowledgeAlarmByNameCommand;
- /// anything else returns an InvalidRequest diagnostic.
+ /// Surfaces the public AcknowledgeAlarm RPC. Acknowledgement is
+ /// session-less: the gateway routes it through the always-on
+ /// monitor session. An
+ /// alarm_full_reference that parses as a canonical GUID forwards
+ /// to AcknowledgeAlarmCommand; a Provider!Group.Tag
+ /// reference forwards to AcknowledgeAlarmByNameCommand; anything
+ /// else returns an InvalidRequest diagnostic in the reply.
///
public override async Task AcknowledgeAlarm(
AcknowledgeAlarmRequest request,
@@ -179,25 +178,12 @@ public sealed class MxAccessGatewayService(
try
{
ArgumentNullException.ThrowIfNull(request);
- if (string.IsNullOrEmpty(request.SessionId))
- {
- throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required."));
- }
if (string.IsNullOrEmpty(request.AlarmFullReference))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "alarm_full_reference is required."));
}
- // Validate the session exists. Throws SessionManagerException → mapped to
- // gRPC NotFound by the caller's MapException.
- _ = ResolveSession(request.SessionId);
-
- // Delegate to the registered alarm dispatcher. DI binds the production
- // WorkerAlarmRpcDispatcher, which routes the ack over the worker IPC by
- // GUID (AcknowledgeAlarmCommand) or by Provider!Group.Tag reference
- // (AcknowledgeAlarmByNameCommand). NotWiredAlarmRpcDispatcher is only the
- // null fallback used when no dispatcher is registered.
- return await alarmRpcDispatcher.AcknowledgeAsync(request, context.CancellationToken)
+ return await alarmService.AcknowledgeAsync(request, context.CancellationToken)
.ConfigureAwait(false);
}
catch (Exception exception) when (exception is not RpcException)
@@ -208,38 +194,27 @@ public sealed class MxAccessGatewayService(
///
///
- /// Surfaces the public QueryActiveAlarms RPC. The gateway validates the request,
- /// resolves the session, and delegates to the registered
- /// . DI binds the production
- /// , which issues a
- /// QueryActiveAlarmsCommand over the worker pipe IPC and streams each
- /// ActiveAlarmSnapshot from the worker reply.
+ /// Surfaces the public StreamAlarms RPC — the session-less central
+ /// alarm feed. The stream opens with one active_alarm per
+ /// currently-active alarm, then a single snapshot_complete, then
+ /// a transition for every subsequent change. Served by the
+ /// gateway's always-on monitor; any
+ /// number of clients fan out from the single monitor.
///
- public override async Task QueryActiveAlarms(
- QueryActiveAlarmsRequest request,
- IServerStreamWriter responseStream,
+ public override async Task StreamAlarms(
+ StreamAlarmsRequest request,
+ IServerStreamWriter responseStream,
ServerCallContext context)
{
try
{
ArgumentNullException.ThrowIfNull(request);
- if (string.IsNullOrEmpty(request.SessionId))
- {
- throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required."));
- }
- _ = ResolveSession(request.SessionId);
-
- // Delegate to the registered alarm dispatcher. DI binds the production
- // WorkerAlarmRpcDispatcher, which issues a QueryActiveAlarmsCommand over the
- // worker IPC and streams each ActiveAlarmSnapshot from the worker reply.
- // NotWiredAlarmRpcDispatcher is only the null fallback used when no
- // dispatcher is registered.
- await foreach (ActiveAlarmSnapshot snapshot in alarmRpcDispatcher
- .QueryActiveAlarmsAsync(request, context.CancellationToken)
+ await foreach (AlarmFeedMessage message in alarmService
+ .StreamAsync(request.AlarmFilterPrefix, context.CancellationToken)
.WithCancellation(context.CancellationToken)
.ConfigureAwait(false))
{
- await responseStream.WriteAsync(snapshot).ConfigureAwait(false);
+ await responseStream.WriteAsync(message).ConfigureAwait(false);
}
}
catch (Exception exception) when (exception is not RpcException)
diff --git a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs
index bd9428a..d134065 100644
--- a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs
+++ b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs
@@ -19,7 +19,7 @@ public sealed class GatewayGrpcScopeResolver
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
diff --git a/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs
deleted file mode 100644
index 41170f3..0000000
--- a/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MxGateway.Contracts.Proto;
-
-namespace MxGateway.Server.Sessions;
-
-///
-/// Gateway-side dispatcher seam for the alarm-RPC surface. Bridges the
-/// public AcknowledgeAlarm + QueryActiveAlarms gRPC handlers
-/// to the worker process that hosts IMxAccessAlarmConsumer.
-///
-///
-///
-/// DI binds the production by
-/// default; it routes calls through the existing worker-pipe IPC.
-/// NotWiredAlarmRpcDispatcher is only the null fallback used
-/// when no dispatcher is registered (DI omission / standalone tests).
-/// Other tests inject a fake to exercise the gateway handler shape
-/// without spinning up a worker process.
-///
-///
-/// The dispatcher is session-scoped: every call resolves the
-/// session and forwards to that session's worker. The handler
-/// constructs the /
-/// stream from the dispatcher's
-/// output without further translation.
-///
-///
-public interface IAlarmRpcDispatcher
-{
- /// Forward an Acknowledge to the worker that owns the session.
- Task AcknowledgeAsync(
- AcknowledgeAlarmRequest request,
- CancellationToken cancellationToken);
-
- /// Walk active alarms on the worker that owns the session.
- IAsyncEnumerable QueryActiveAlarmsAsync(
- QueryActiveAlarmsRequest request,
- CancellationToken cancellationToken);
-}
diff --git a/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs
deleted file mode 100644
index 2e5306d..0000000
--- a/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MxGateway.Contracts.Proto;
-using MxGateway.Server.Grpc;
-
-namespace MxGateway.Server.Sessions;
-
-///
-/// Null fallback used when no dispatcher
-/// is registered in the DI container (DI omission or standalone tests).
-/// Acknowledges with a structured "alarm dispatcher not registered"
-/// diagnostic and yields an empty active-alarm stream.
-///
-///
-///
-/// Production wires as the
-/// default via
-/// SessionServiceCollectionExtensions.AddGatewaySessions, so
-/// clients that hit this fallback are running against an
-/// intentionally minimal service composition rather than the full
-/// gateway.
-///
-///
-public sealed class NotWiredAlarmRpcDispatcher : IAlarmRpcDispatcher
-{
- ///
- public Task AcknowledgeAsync(
- AcknowledgeAlarmRequest request,
- CancellationToken cancellationToken)
- {
- return Task.FromResult(new AcknowledgeAlarmReply
- {
- SessionId = request.SessionId,
- CorrelationId = request.ClientCorrelationId,
- ProtocolStatus = MxAccessGrpcMapper.Ok("AcknowledgeAlarm accepted; alarm dispatcher is not registered."),
- DiagnosticMessage = "Alarm dispatcher is not registered.",
- });
- }
-
- ///
-#pragma warning disable CS1998 // Async method lacks 'await' operators — empty stream is intentional.
- public async IAsyncEnumerable QueryActiveAlarmsAsync(
- QueryActiveAlarmsRequest request,
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- yield break;
- }
-#pragma warning restore CS1998
-}
diff --git a/src/MxGateway.Server/Sessions/SessionManager.cs b/src/MxGateway.Server/Sessions/SessionManager.cs
index 488c2f8..4a0ee21 100644
--- a/src/MxGateway.Server/Sessions/SessionManager.cs
+++ b/src/MxGateway.Server/Sessions/SessionManager.cs
@@ -90,8 +90,6 @@ public sealed class SessionManager : ISessionManager
_metrics.SessionOpened();
sessionOpenedRecorded = true;
- await TryAutoSubscribeAlarmsAsync(session, cancellationToken).ConfigureAwait(false);
-
return session;
}
catch (Exception exception)
@@ -410,100 +408,4 @@ public sealed class SessionManager : ISessionManager
return Convert.ToBase64String(bytes);
}
- ///
- /// If Alarms.Enabled is configured, issue a
- /// SubscribeAlarmsCommand on the freshly-Ready session so the
- /// worker's wnwrap consumer starts polling. Failure handling is
- /// governed by Alarms.RequireSubscribeOnOpen:
- ///
- /// - true — propagate the failure to fault the session.
- /// - false (default) — log a warning and let the session continue serving data subscriptions.
- ///
- ///
- private async Task TryAutoSubscribeAlarmsAsync(
- GatewaySession session,
- CancellationToken cancellationToken)
- {
- AlarmsOptions alarms = _options.Alarms;
- if (!alarms.Enabled) return;
-
- string subscription = ResolveAlarmSubscription(alarms);
- if (string.IsNullOrWhiteSpace(subscription))
- {
- const string diagnostic =
- "Alarms.Enabled is true but no SubscriptionExpression / DefaultArea is configured.";
- if (alarms.RequireSubscribeOnOpen)
- {
- throw new SessionManagerException(
- SessionManagerErrorCode.OpenFailed, diagnostic);
- }
- _logger.LogWarning(
- "Auto-subscribe skipped for session {SessionId}: {Diagnostic}",
- session.SessionId, diagnostic);
- return;
- }
-
- WorkerCommand command = new WorkerCommand
- {
- Command = new MxCommand
- {
- Kind = MxCommandKind.SubscribeAlarms,
- SubscribeAlarms = new SubscribeAlarmsCommand
- {
- SubscriptionExpression = subscription,
- },
- },
- EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()),
- };
-
- try
- {
- WorkerCommandReply reply = await session.InvokeAsync(command, cancellationToken)
- .ConfigureAwait(false);
- ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code;
- if (code != ProtocolStatusCode.Ok)
- {
- string diagnostic = reply.Reply?.DiagnosticMessage
- ?? reply.Reply?.ProtocolStatus?.Message
- ?? "Worker rejected SubscribeAlarms.";
- if (alarms.RequireSubscribeOnOpen)
- {
- throw new SessionManagerException(
- SessionManagerErrorCode.OpenFailed,
- $"Auto-subscribe failed for session {session.SessionId}: {diagnostic}");
- }
- _logger.LogWarning(
- "Auto-subscribe failed for session {SessionId} (status {StatusCode}): {Diagnostic}",
- session.SessionId, code, diagnostic);
- return;
- }
- _logger.LogInformation(
- "Alarm auto-subscribe succeeded for session {SessionId} on {Subscription}.",
- session.SessionId, subscription);
- }
- catch (SessionManagerException)
- {
- throw;
- }
- catch (Exception ex) when (!alarms.RequireSubscribeOnOpen)
- {
- _logger.LogWarning(
- ex,
- "Auto-subscribe threw for session {SessionId} on {Subscription}; alarm path remains inactive.",
- session.SessionId, subscription);
- }
- }
-
- private static string ResolveAlarmSubscription(AlarmsOptions alarms)
- {
- if (!string.IsNullOrWhiteSpace(alarms.SubscriptionExpression))
- {
- return alarms.SubscriptionExpression;
- }
- if (!string.IsNullOrWhiteSpace(alarms.DefaultArea))
- {
- return $@"\\{Environment.MachineName}\Galaxy!{alarms.DefaultArea}";
- }
- return string.Empty;
- }
}
diff --git a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs
index 1f1e67b..5b7eae4 100644
--- a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs
+++ b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs
@@ -11,7 +11,6 @@ public static class SessionServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddHostedService();
services.AddHostedService();
diff --git a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs
deleted file mode 100644
index ef19f52..0000000
--- a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-using System.Runtime.CompilerServices;
-using Google.Protobuf.WellKnownTypes;
-using MxGateway.Contracts.Proto;
-using MxGateway.Server.Grpc;
-
-namespace MxGateway.Server.Sessions;
-
-///
-/// Production that routes the public
-/// AcknowledgeAlarm + QueryActiveAlarms RPCs through the
-/// worker pipe IPC. DI binds this dispatcher;
-/// is only the null fallback used when no dispatcher is registered.
-///
-///
-///
-/// QueryActiveAlarms issues a
-/// over the pipe and yields
-/// each from the
-/// .
-///
-///
-/// AcknowledgeAlarm accepts either form of
-/// : a canonical
-/// GUID forwards as an ; a
-/// Provider!Group.Tag reference is parsed by
-/// and forwarded as an
-/// . Any other reference
-/// returns an InvalidRequest diagnostic.
-///
-///
-public sealed class WorkerAlarmRpcDispatcher(
- ISessionRegistry sessionRegistry,
- TimeProvider? timeProvider = null) : IAlarmRpcDispatcher
-{
- private readonly ISessionRegistry sessionRegistry = sessionRegistry
- ?? throw new ArgumentNullException(nameof(sessionRegistry));
- private readonly TimeProvider timeProvider = timeProvider ?? TimeProvider.System;
-
- ///
- /// Parse a full alarm reference of the form Provider!Group.Tag
- /// into its components. Convention: the first ! separates
- /// provider from Group.Tag; the first . after the
- /// ! separates group from tag (the tag itself may contain
- /// more dots — e.g. TestMachine_001.TestAlarm001).
- ///
- /// true on a well-formed reference; false otherwise.
- public static bool TryParseAlarmReference(
- string? reference,
- out string providerName,
- out string groupName,
- out string alarmName)
- {
- providerName = string.Empty;
- groupName = string.Empty;
- alarmName = string.Empty;
- if (string.IsNullOrWhiteSpace(reference)) return false;
-
- int bang = reference!.IndexOf('!');
- if (bang <= 0 || bang == reference.Length - 1) return false;
-
- string left = reference[..bang];
- string right = reference[(bang + 1)..];
- int dot = right.IndexOf('.');
- if (dot <= 0 || dot == right.Length - 1) return false;
-
- providerName = left;
- groupName = right[..dot];
- alarmName = right[(dot + 1)..];
- return true;
- }
-
- ///
- public async Task AcknowledgeAsync(
- AcknowledgeAlarmRequest request,
- CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(request);
-
- if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession? session) || session is null)
- {
- return new AcknowledgeAlarmReply
- {
- SessionId = request.SessionId,
- CorrelationId = request.ClientCorrelationId,
- ProtocolStatus = MxAccessGrpcMapper.SessionNotFound(
- $"Session '{request.SessionId}' not found."),
- DiagnosticMessage = "AcknowledgeAlarm: session not found.",
- };
- }
-
- WorkerCommand workerCommand;
- if (Guid.TryParse(request.AlarmFullReference, out Guid guid))
- {
- workerCommand = new WorkerCommand
- {
- Command = new MxCommand
- {
- Kind = MxCommandKind.AcknowledgeAlarm,
- AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
- {
- AlarmGuid = guid.ToString(),
- Comment = request.Comment ?? string.Empty,
- OperatorUser = request.OperatorUser ?? string.Empty,
- // Operator node/domain/full-name are not on the public
- // RPC surface today; pass empty strings so the worker
- // honours the existing AcknowledgeAlarmCommand schema.
- OperatorNode = string.Empty,
- OperatorDomain = string.Empty,
- OperatorFullName = string.Empty,
- },
- },
- EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
- };
- }
- else if (TryParseAlarmReference(
- request.AlarmFullReference,
- out string providerName,
- out string groupName,
- out string alarmName))
- {
- workerCommand = new WorkerCommand
- {
- Command = new MxCommand
- {
- Kind = MxCommandKind.AcknowledgeAlarmByName,
- AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
- {
- AlarmName = alarmName,
- ProviderName = providerName,
- GroupName = groupName,
- Comment = request.Comment ?? string.Empty,
- OperatorUser = request.OperatorUser ?? string.Empty,
- OperatorNode = string.Empty,
- OperatorDomain = string.Empty,
- OperatorFullName = string.Empty,
- },
- },
- EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
- };
- }
- else
- {
- return new AcknowledgeAlarmReply
- {
- SessionId = request.SessionId,
- CorrelationId = request.ClientCorrelationId,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.InvalidRequest,
- Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.",
- },
- DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.",
- };
- }
-
- WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
- .ConfigureAwait(false);
-
- MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply
- {
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.ProtocolViolation,
- Message = "Worker reply did not include an MxCommandReply.",
- },
- };
-
- AcknowledgeAlarmReply reply = new AcknowledgeAlarmReply
- {
- SessionId = request.SessionId,
- CorrelationId = request.ClientCorrelationId,
- ProtocolStatus = mxReply.ProtocolStatus ?? MxAccessGrpcMapper.Ok(),
- DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty,
- };
- if (mxReply.HasHresult)
- {
- reply.Hresult = mxReply.Hresult;
- }
- return reply;
- }
-
- ///
- public async IAsyncEnumerable QueryActiveAlarmsAsync(
- QueryActiveAlarmsRequest request,
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(request);
-
- if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession? session) || session is null)
- {
- // Server-019: align with AcknowledgeAsync's missing-session handling and
- // surface a SessionNotFound error rather than yielding an empty stream.
- // QueryActiveAlarms is server-streaming, so a thrown exception is the
- // cleaner fit than an in-band ProtocolStatus; MxAccessGatewayService maps
- // SessionManagerException(SessionNotFound) to gRPC NotFound.
- throw new SessionManagerException(
- SessionManagerErrorCode.SessionNotFound,
- $"Session '{request.SessionId}' not found.");
- }
-
- WorkerCommand workerCommand = new WorkerCommand
- {
- Command = new MxCommand
- {
- Kind = MxCommandKind.QueryActiveAlarms,
- QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand
- {
- AlarmFilterPrefix = request.AlarmFilterPrefix ?? string.Empty,
- },
- },
- EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
- };
-
- WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
- .ConfigureAwait(false);
-
- MxCommandReply? mxReply = workerReply.Reply;
- if (mxReply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok) yield break;
-
- QueryActiveAlarmsReplyPayload? payload = mxReply.QueryActiveAlarms;
- if (payload is null) yield break;
-
- foreach (ActiveAlarmSnapshot snapshot in payload.Snapshots)
- {
- cancellationToken.ThrowIfCancellationRequested();
- yield return snapshot;
- }
- }
-}
diff --git a/src/MxGateway.Server/appsettings.json b/src/MxGateway.Server/appsettings.json
index 2c5a91b..4d4da73 100644
--- a/src/MxGateway.Server/appsettings.json
+++ b/src/MxGateway.Server/appsettings.json
@@ -71,7 +71,7 @@
"Enabled": true,
"SubscriptionExpression": "\\\\DESKTOP-6JL3KKO\\Galaxy!DEV",
"DefaultArea": "",
- "RequireSubscribeOnOpen": false
+ "ReconcileIntervalSeconds": 30
}
}
}
diff --git a/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs b/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
index 0ad0edd..b36d59a 100644
--- a/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
+++ b/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
@@ -21,7 +21,7 @@ public sealed class ProtobufContractRoundTripTests
Assert.Contains(service.Methods, method => method.Name == "Invoke");
Assert.Contains(service.Methods, method => method.Name == "StreamEvents");
Assert.Contains(service.Methods, method => method.Name == "AcknowledgeAlarm");
- Assert.Contains(service.Methods, method => method.Name == "QueryActiveAlarms");
+ Assert.Contains(service.Methods, method => method.Name == "StreamAlarms");
}
/// Verifies that worker envelope descriptor contains required correlation fields.
@@ -306,7 +306,6 @@ public sealed class ProtobufContractRoundTripTests
{
var original = new AcknowledgeAlarmRequest
{
- SessionId = "session-1",
ClientCorrelationId = "client-correlation-7",
AlarmFullReference = "Tank01.Level.HiHi",
Comment = "shift handover",
@@ -324,7 +323,6 @@ public sealed class ProtobufContractRoundTripTests
{
var original = new AcknowledgeAlarmReply
{
- SessionId = "session-1",
CorrelationId = "gateway-correlation-7",
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
Hresult = 0,
@@ -420,25 +418,23 @@ public sealed class ProtobufContractRoundTripTests
Assert.Equal(AlarmConditionState.ActiveAcked, parsed.CurrentState);
}
- /// Verifies that QueryActiveAlarmsRequest round-trips empty filter prefix.
+ /// Verifies that StreamAlarmsRequest round-trips with and without a filter prefix.
[Fact]
- public void QueryActiveAlarmsRequest_RoundTripsWithAndWithoutFilter()
+ public void StreamAlarmsRequest_RoundTripsWithAndWithoutFilter()
{
- var withoutFilter = new QueryActiveAlarmsRequest
+ var withoutFilter = new StreamAlarmsRequest
{
- SessionId = "session-1",
ClientCorrelationId = "client-correlation-8",
};
- var withFilter = new QueryActiveAlarmsRequest
+ var withFilter = new StreamAlarmsRequest
{
- SessionId = "session-1",
ClientCorrelationId = "client-correlation-9",
AlarmFilterPrefix = "Tank01.",
};
- Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray()));
- Assert.Equal(withFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray()));
+ Assert.Equal(withoutFilter, StreamAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray()));
+ Assert.Equal(withFilter, StreamAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray()));
}
/// Verifies that an MxValue carrying a raw_value bytes payload round-trips.
diff --git a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
index 29ae84d..0c06f56 100644
--- a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
+++ b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
@@ -188,7 +188,8 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
mapper,
eventStreamService,
_metrics,
- NullLogger.Instance);
+ NullLogger.Instance,
+ new FakeGatewayAlarmService());
}
///
diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
index fcb276e..fe04d10 100644
--- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
+++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
@@ -612,7 +612,8 @@ public sealed class MxAccessGatewayServiceConstraintTests
new MxAccessGrpcMapper(),
new FakeEventStreamService(sessionManager),
new GatewayMetrics(),
- NullLogger.Instance);
+ NullLogger.Instance,
+ new FakeGatewayAlarmService());
}
private static FakeSessionManager CreateSessionManagerWithSeed()
diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
index 97be331..7aeef0b 100644
--- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
+++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
@@ -92,54 +92,6 @@ public sealed class MxAccessGatewayServiceTests
Assert.Equal(1, sessionManager.InvokeCount);
}
- ///
- /// Verifies that AcknowledgeAlarm maps a genuinely missing session to NotFound via
- /// the service's own ResolveSession lookup rather than an injected exception.
- ///
- [Fact]
- public async Task AcknowledgeAlarm_WhenSessionMissing_ThrowsNotFound()
- {
- FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true };
- MxAccessGatewayService service = CreateService(sessionManager);
-
- RpcException exception = await Assert.ThrowsAsync(
- async () => await service.AcknowledgeAlarm(
- new AcknowledgeAlarmRequest
- {
- SessionId = "session-missing",
- AlarmFullReference = "Tank01.Level.HiHi",
- OperatorUser = "alice",
- },
- new TestServerCallContext()));
-
- Assert.Equal(StatusCode.NotFound, exception.StatusCode);
- Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal);
- }
-
- ///
- /// Verifies that QueryActiveAlarms maps a genuinely missing session to NotFound via
- /// the service's own ResolveSession lookup rather than an injected exception.
- ///
- [Fact]
- public async Task QueryActiveAlarms_WhenSessionMissing_ThrowsNotFound()
- {
- FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true };
- MxAccessGatewayService service = CreateService(sessionManager);
-
- RpcException exception = await Assert.ThrowsAsync(
- async () => await service.QueryActiveAlarms(
- new QueryActiveAlarmsRequest
- {
- SessionId = "session-missing",
- AlarmFilterPrefix = "Tank01.",
- },
- new RecordingServerStreamWriter(),
- new TestServerCallContext()));
-
- Assert.Equal(StatusCode.NotFound, exception.StatusCode);
- Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal);
- }
-
/// Verifies that Invoke throws InvalidArgument and does not invoke the session manager when payload is mismatched.
[Fact]
public async Task Invoke_WithMismatchedPayload_ThrowsInvalidArgumentAndDoesNotCallSessionManager()
@@ -301,32 +253,13 @@ public sealed class MxAccessGatewayServiceTests
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
}
- // ===== PR A.4 — AcknowledgeAlarm + QueryActiveAlarms handler contract =====
+ // ===== AcknowledgeAlarm + StreamAlarms handler contract =====
//
- // Worker-side dispatch (translating AcknowledgeAlarm to MxAccess Acknowledge,
- // walking the active-alarm collection for QueryActiveAlarms) is gated on PR
- // A.2's dev-rig validation. These tests pin the public surface so the worker
- // wiring lands without changing observable behaviour for clients.
+ // AcknowledgeAlarm validates alarm_full_reference then delegates to the
+ // session-less IGatewayAlarmService; StreamAlarms forwards the central
+ // alarm feed. CreateService injects FakeGatewayAlarmService.
- /// Verifies AcknowledgeAlarm rejects empty session_id.
- [Fact]
- public async Task AcknowledgeAlarm_WithMissingSessionId_ThrowsInvalidArgument()
- {
- MxAccessGatewayService service = CreateService(new FakeSessionManager());
-
- RpcException exception = await Assert.ThrowsAsync(
- async () => await service.AcknowledgeAlarm(
- new AcknowledgeAlarmRequest
- {
- AlarmFullReference = "Tank01.Level.HiHi",
- OperatorUser = "alice",
- },
- new TestServerCallContext()));
-
- Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
- }
-
- /// Verifies AcknowledgeAlarm rejects empty alarm_full_reference.
+ /// Verifies AcknowledgeAlarm rejects an empty alarm_full_reference.
[Fact]
public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument()
{
@@ -334,71 +267,47 @@ public sealed class MxAccessGatewayServiceTests
RpcException exception = await Assert.ThrowsAsync(
async () => await service.AcknowledgeAlarm(
- new AcknowledgeAlarmRequest
- {
- SessionId = "session-1",
- OperatorUser = "alice",
- },
+ new AcknowledgeAlarmRequest { OperatorUser = "alice" },
new TestServerCallContext()));
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
}
- /// Verifies AcknowledgeAlarm returns OK with a "dispatcher not registered" diagnostic when DI omits the dispatcher.
+ /// Verifies AcknowledgeAlarm delegates a valid request to the alarm service.
[Fact]
- public async Task AcknowledgeAlarm_WithValidRequest_ReturnsOkWithNotRegisteredDiagnostic()
+ public async Task AcknowledgeAlarm_WithValidRequest_DelegatesToAlarmService()
{
MxAccessGatewayService service = CreateService(new FakeSessionManager());
AcknowledgeAlarmReply reply = await service.AcknowledgeAlarm(
new AcknowledgeAlarmRequest
{
- SessionId = "session-1",
ClientCorrelationId = "corr-1",
- AlarmFullReference = "Tank01.Level.HiHi",
+ AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi",
Comment = "investigating",
OperatorUser = "alice",
},
new TestServerCallContext());
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
- Assert.Equal("session-1", reply.SessionId);
Assert.Equal("corr-1", reply.CorrelationId);
- Assert.Contains("not registered", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
}
- /// Verifies QueryActiveAlarms rejects empty session_id.
+ /// Verifies StreamAlarms forwards the central alarm feed, ending with snapshot_complete.
[Fact]
- public async Task QueryActiveAlarms_WithMissingSessionId_ThrowsInvalidArgument()
+ public async Task StreamAlarms_ForwardsTheCentralAlarmFeed()
{
MxAccessGatewayService service = CreateService(new FakeSessionManager());
+ RecordingServerStreamWriter sink = new();
- RpcException exception = await Assert.ThrowsAsync(
- async () => await service.QueryActiveAlarms(
- new QueryActiveAlarmsRequest(),
- new RecordingServerStreamWriter(),
- new TestServerCallContext()));
-
- Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
- }
-
- /// Verifies QueryActiveAlarms streams zero snapshots until PR A.2 wires the worker walk.
- [Fact]
- public async Task QueryActiveAlarms_WithValidRequest_StreamsZeroSnapshots()
- {
- MxAccessGatewayService service = CreateService(new FakeSessionManager());
- RecordingServerStreamWriter sink = new();
-
- await service.QueryActiveAlarms(
- new QueryActiveAlarmsRequest
- {
- SessionId = "session-1",
- AlarmFilterPrefix = "Tank01.",
- },
+ await service.StreamAlarms(
+ new StreamAlarmsRequest(),
sink,
new TestServerCallContext());
- Assert.Empty(sink.Messages);
+ Assert.Contains(
+ sink.Messages,
+ message => message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotComplete);
}
/// Verifies OpenSession advertises the alarm RPC capability strings.
@@ -433,7 +342,8 @@ public sealed class MxAccessGatewayServiceTests
new MxAccessGrpcMapper(),
new FakeEventStreamService(sessionManager),
metrics ?? new GatewayMetrics(),
- NullLogger.Instance);
+ NullLogger.Instance,
+ new FakeGatewayAlarmService());
}
private static ApiKeyIdentity CreateIdentity()
diff --git a/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs
deleted file mode 100644
index 4b5f2e8..0000000
--- a/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using MxGateway.Contracts.Proto;
-using MxGateway.Server.Sessions;
-
-namespace MxGateway.Tests.Gateway.Sessions;
-
-///
-/// Pins the null-fallback dispatcher's behaviour: AcknowledgeAsync
-/// returns OK with a "dispatcher not registered" diagnostic and
-/// QueryActiveAlarmsAsync yields an empty stream. Production binds
-/// WorkerAlarmRpcDispatcher in DI; this fallback is only used
-/// when no dispatcher is registered (DI omission / standalone tests).
-///
-public sealed class NotWiredAlarmRpcDispatcherTests
-{
- [Fact]
- public async Task AcknowledgeAsync_WhenNotWired_ReturnsOkWithNotRegisteredDiagnostic()
- {
- IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher();
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "session-1",
- ClientCorrelationId = "corr-1",
- AlarmFullReference = "Tank01.Level.HiHi",
- Comment = "investigating",
- OperatorUser = "alice",
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
- Assert.Equal("session-1", reply.SessionId);
- Assert.Equal("corr-1", reply.CorrelationId);
- Assert.Contains("not registered", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
- }
-
- [Fact]
- public async Task QueryActiveAlarmsAsync_WhenNotWired_YieldsNoSnapshots()
- {
- IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher();
-
- int count = 0;
- await foreach (ActiveAlarmSnapshot _ in dispatcher.QueryActiveAlarmsAsync(
- new QueryActiveAlarmsRequest { SessionId = "session-1" },
- CancellationToken.None))
- {
- count++;
- }
-
- Assert.Equal(0, count);
- }
-}
diff --git a/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs b/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs
deleted file mode 100644
index e4c3175..0000000
--- a/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs
+++ /dev/null
@@ -1,305 +0,0 @@
-using System.Runtime.CompilerServices;
-using Google.Protobuf.WellKnownTypes;
-using Microsoft.Extensions.Options;
-using MxGateway.Contracts.Proto;
-using MxGateway.Server.Configuration;
-using MxGateway.Server.Metrics;
-using MxGateway.Server.Sessions;
-using MxGateway.Server.Workers;
-
-namespace MxGateway.Tests.Gateway.Sessions;
-
-///
-/// Pins the alarm auto-subscribe hook on session open. Runs in
-/// its own file because the cases are orthogonal to
-/// (alarms-disabled vs.
-/// alarms-enabled lanes), and the fake worker client below verifies
-/// the issued SubscribeAlarms command shape directly.
-///
-public sealed class SessionManagerAlarmAutoSubscribeTests
-{
- [Fact]
- public async Task OpenSessionAsync_DoesNotAutoSubscribe_WhenAlarmsDisabled()
- {
- AlarmAutoSubscribeWorkerClient worker = new();
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions { Enabled = false });
-
- await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
-
- Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
- }
-
- [Fact]
- public async Task OpenSessionAsync_AutoSubscribes_WhenEnabledWithExpression()
- {
- AlarmAutoSubscribeWorkerClient worker = new();
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- SubscriptionExpression = @"\\HOST\Galaxy!Area1",
- });
-
- GatewaySession session = await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None);
-
- Assert.Equal(SessionState.Ready, session.State);
- Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
- Assert.Equal(@"\\HOST\Galaxy!Area1",
- worker.LastSubscribeAlarmsCommand!.SubscriptionExpression);
- }
-
- [Fact]
- public async Task OpenSessionAsync_FallsBackToDefaultArea_WhenExpressionEmpty()
- {
- AlarmAutoSubscribeWorkerClient worker = new();
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- DefaultArea = "DEV",
- });
-
- await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
-
- Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
- Assert.Contains(@"\Galaxy!DEV",
- worker.LastSubscribeAlarmsCommand!.SubscriptionExpression);
- }
-
- [Fact]
- public async Task OpenSessionAsync_Succeeds_WhenAutoSubscribeFailsWithRequireOff()
- {
- // Worker rejects the SubscribeAlarms command. With RequireSubscribeOnOpen=false
- // (the default), the session still opens — alarm-side commands later return
- // "not subscribed", but data subscriptions work.
- AlarmAutoSubscribeWorkerClient worker = new()
- {
- SubscribeAlarmsReplyFactory = _ => new MxCommandReply
- {
- Kind = MxCommandKind.SubscribeAlarms,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.MxaccessFailure,
- Message = "wnwrap subscribe failed",
- },
- DiagnosticMessage = "alarm provider unavailable",
- },
- };
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- SubscriptionExpression = @"\\HOST\Galaxy!Area1",
- RequireSubscribeOnOpen = false,
- });
-
- GatewaySession session = await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None);
-
- Assert.Equal(SessionState.Ready, session.State);
- Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
- }
-
- [Fact]
- public async Task OpenSessionAsync_Throws_WhenAutoSubscribeFailsWithRequireOn()
- {
- AlarmAutoSubscribeWorkerClient worker = new()
- {
- SubscribeAlarmsReplyFactory = _ => new MxCommandReply
- {
- Kind = MxCommandKind.SubscribeAlarms,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.MxaccessFailure,
- Message = "wnwrap subscribe failed",
- },
- },
- };
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- SubscriptionExpression = @"\\HOST\Galaxy!Area1",
- RequireSubscribeOnOpen = true,
- });
-
- await Assert.ThrowsAsync(
- async () => await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None));
- }
-
- ///
- /// Server-006 regression: when auto-subscribe throws after
- /// SessionOpened() incremented the open-session gauge, the failed
- /// open must not leave mxgateway.sessions.open over-counted.
- ///
- [Fact]
- public async Task OpenSessionAsync_DoesNotLeakOpenSessionGauge_WhenAutoSubscribeFailsWithRequireOn()
- {
- AlarmAutoSubscribeWorkerClient worker = new()
- {
- SubscribeAlarmsReplyFactory = _ => new MxCommandReply
- {
- Kind = MxCommandKind.SubscribeAlarms,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.MxaccessFailure,
- Message = "wnwrap subscribe failed",
- },
- },
- };
- using GatewayMetrics metrics = new();
- SessionManager manager = NewManager(
- worker,
- alarms: new AlarmsOptions
- {
- Enabled = true,
- SubscriptionExpression = @"\\HOST\Galaxy!Area1",
- RequireSubscribeOnOpen = true,
- },
- metrics: metrics);
-
- await Assert.ThrowsAsync(
- async () => await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None));
-
- Assert.Equal(0, metrics.GetSnapshot().OpenSessions);
- }
-
- [Fact]
- public async Task OpenSessionAsync_Throws_WhenEnabledButNoExpressionAndRequireOn()
- {
- AlarmAutoSubscribeWorkerClient worker = new();
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- // No SubscriptionExpression and no DefaultArea.
- RequireSubscribeOnOpen = true,
- });
-
- await Assert.ThrowsAsync(
- async () => await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None));
- Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
- }
-
- [Fact]
- public async Task OpenSessionAsync_Succeeds_WhenEnabledButNoExpressionAndRequireOff()
- {
- AlarmAutoSubscribeWorkerClient worker = new();
- SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
- {
- Enabled = true,
- // No SubscriptionExpression and no DefaultArea — default require=false.
- });
-
- GatewaySession session = await manager.OpenSessionAsync(
- CreateOpenRequest(), "client-1", CancellationToken.None);
-
- Assert.Equal(SessionState.Ready, session.State);
- Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
- }
-
- private static SessionManager NewManager(
- AlarmAutoSubscribeWorkerClient worker,
- AlarmsOptions alarms,
- GatewayMetrics? metrics = null)
- {
- FakeSessionWorkerClientFactory factory = new(worker);
- GatewayOptions options = new GatewayOptions
- {
- Sessions = new SessionOptions
- {
- DefaultCommandTimeoutSeconds = 30,
- MaxSessions = 64,
- DefaultLeaseSeconds = 1800,
- },
- Worker = new WorkerOptions
- {
- StartupTimeoutSeconds = 30,
- ShutdownTimeoutSeconds = 10,
- },
- Alarms = alarms,
- };
- return new SessionManager(
- new SessionRegistry(),
- factory,
- Options.Create(options),
- metrics ?? new GatewayMetrics());
- }
-
- private static SessionOpenRequest CreateOpenRequest()
- {
- return new SessionOpenRequest(
- RequestedBackend: null,
- ClientSessionName: "test-session",
- ClientCorrelationId: "client-correlation-1",
- CommandTimeout: Duration.FromTimeSpan(TimeSpan.FromSeconds(5)));
- }
-
- private sealed class FakeSessionWorkerClientFactory(IWorkerClient client) : ISessionWorkerClientFactory
- {
- public Task CreateAsync(
- GatewaySession session,
- CancellationToken cancellationToken)
- {
- return Task.FromResult(client);
- }
- }
-
- private sealed class AlarmAutoSubscribeWorkerClient : IWorkerClient
- {
- public string SessionId { get; } = "session-1";
- public int? ProcessId { get; } = 1234;
- public WorkerClientState State { get; set; } = WorkerClientState.Ready;
- public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
-
- public int SubscribeAlarmsInvokeCount { get; private set; }
- public SubscribeAlarmsCommand? LastSubscribeAlarmsCommand { get; private set; }
- public Func? SubscribeAlarmsReplyFactory { get; init; }
-
- public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- public Task InvokeAsync(
- WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken)
- {
- if (command.Command?.Kind == MxCommandKind.SubscribeAlarms)
- {
- SubscribeAlarmsInvokeCount++;
- LastSubscribeAlarmsCommand = command.Command.SubscribeAlarms;
- MxCommandReply reply = SubscribeAlarmsReplyFactory?.Invoke(command)
- ?? new MxCommandReply
- {
- Kind = MxCommandKind.SubscribeAlarms,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.Ok,
- Message = "OK",
- },
- };
- return Task.FromResult(new WorkerCommandReply { Reply = reply });
- }
- return Task.FromResult(new WorkerCommandReply
- {
- Reply = new MxCommandReply
- {
- Kind = command.Command?.Kind ?? MxCommandKind.Unspecified,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.Ok,
- Message = "OK",
- },
- },
- });
- }
-
- public async IAsyncEnumerable ReadEventsAsync(
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- await Task.CompletedTask;
- yield break;
- }
-
- public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken)
- => Task.CompletedTask;
- public void Kill(string reason) { }
- public ValueTask DisposeAsync() => ValueTask.CompletedTask;
- }
-}
diff --git a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs
deleted file mode 100644
index d6c7367..0000000
--- a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs
+++ /dev/null
@@ -1,393 +0,0 @@
-using System.Runtime.CompilerServices;
-using MxGateway.Contracts.Proto;
-using MxGateway.Server.Sessions;
-using MxGateway.Server.Workers;
-
-namespace MxGateway.Tests.Gateway.Sessions;
-
-///
-/// Pins the production 's behaviour:
-/// resolves the session by id, issues the matching MxCommand over the
-/// worker pipe, and unwraps the reply into AcknowledgeAlarmReply or the
-/// ActiveAlarmSnapshot stream.
-///
-public sealed class WorkerAlarmRpcDispatcherTests
-{
- [Fact]
- public async Task AcknowledgeAsync_WhenSessionMissing_ReturnsSessionNotFound()
- {
- SessionRegistry registry = new();
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "missing",
- ClientCorrelationId = "c1",
- AlarmFullReference = Guid.NewGuid().ToString(),
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code);
- }
-
- [Fact]
- public async Task AcknowledgeAsync_WithGuidReference_ForwardsGuidAndReturnsNativeStatus()
- {
- SessionRegistry registry = new();
- Guid alarmGuid = Guid.NewGuid();
- FakeAlarmWorkerClient worker = new()
- {
- ReplyFactory = command =>
- {
- Assert.Equal(MxCommandKind.AcknowledgeAlarm, command.Command.Kind);
- Assert.Equal(alarmGuid.ToString(), command.Command.AcknowledgeAlarmCommand.AlarmGuid);
- Assert.Equal("ack", command.Command.AcknowledgeAlarmCommand.Comment);
- Assert.Equal("alice", command.Command.AcknowledgeAlarmCommand.OperatorUser);
- return new MxCommandReply
- {
- Kind = MxCommandKind.AcknowledgeAlarm,
- ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
- Hresult = 0,
- AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
- };
- },
- };
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "s1",
- ClientCorrelationId = "c1",
- AlarmFullReference = alarmGuid.ToString(),
- Comment = "ack",
- OperatorUser = "alice",
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
- Assert.Equal(0, reply.Hresult);
- Assert.Equal("s1", reply.SessionId);
- Assert.Equal("c1", reply.CorrelationId);
- Assert.Equal(1, worker.InvokeCount);
- }
-
- [Fact]
- public async Task AcknowledgeAsync_WhenWorkerFails_PropagatesWorkerDiagnostic()
- {
- SessionRegistry registry = new();
- FakeAlarmWorkerClient worker = new()
- {
- ReplyFactory = _ => new MxCommandReply
- {
- Kind = MxCommandKind.AcknowledgeAlarm,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.MxaccessFailure,
- Message = "AVEVA Acknowledge failed.",
- },
- Hresult = -123,
- DiagnosticMessage = "AVEVA AlarmAckByGUID returned non-zero status -123.",
- },
- };
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "s1",
- AlarmFullReference = Guid.NewGuid().ToString(),
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code);
- Assert.Equal(-123, reply.Hresult);
- Assert.Contains("-123", reply.DiagnosticMessage);
- }
-
- [Theory]
- [InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")]
- [InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")]
- [InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")]
- public void TryParseAlarmReference_WithProviderGroupTag_DecomposesParts(
- string reference, string expectedProvider, string expectedGroup, string expectedName)
- {
- Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
- reference, out string provider, out string group, out string name));
- Assert.Equal(expectedProvider, provider);
- Assert.Equal(expectedGroup, group);
- Assert.Equal(expectedName, name);
- }
-
- [Theory]
- [InlineData("")]
- [InlineData(" ")]
- [InlineData(null)]
- [InlineData("no-bang-here")]
- [InlineData("!Group.Tag")] // empty provider
- [InlineData("Galaxy!")] // bang at end
- [InlineData("Galaxy!Group")] // missing dot
- [InlineData("Galaxy!.Tag")] // empty group
- [InlineData("Galaxy!Group.")] // empty tag
- public void TryParseAlarmReference_WithMalformedReference_ReturnsFalse(string? reference)
- {
- Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
- reference, out _, out _, out _));
- }
-
- [Fact]
- public async Task AcknowledgeAsync_WithProviderGroupTagReference_RoutesViaAckByName()
- {
- SessionRegistry registry = new();
- AcknowledgeAlarmByNameCommand? observed = null;
- FakeAlarmWorkerClient worker = new()
- {
- ReplyFactory = command =>
- {
- Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind);
- observed = command.Command.AcknowledgeAlarmByNameCommand;
- return new MxCommandReply
- {
- Kind = MxCommandKind.AcknowledgeAlarmByName,
- ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
- Hresult = 0,
- AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
- };
- },
- };
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "s1",
- ClientCorrelationId = "c1",
- AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
- Comment = "ack-by-name",
- OperatorUser = "bob",
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
- Assert.NotNull(observed);
- Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName);
- Assert.Equal("Galaxy", observed.ProviderName);
- Assert.Equal("TestArea", observed.GroupName);
- Assert.Equal("bob", observed.OperatorUser);
- Assert.Equal("ack-by-name", observed.Comment);
- }
-
- [Fact]
- public async Task AcknowledgeAsync_WithUnparseableReference_ReturnsInvalidRequest()
- {
- SessionRegistry registry = new();
- FakeAlarmWorkerClient worker = new();
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "s1",
- AlarmFullReference = "no-bang-no-dot",
- },
- CancellationToken.None);
-
- Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
- Assert.Equal(0, worker.InvokeCount);
- }
-
- [Fact]
- public async Task QueryActiveAlarmsAsync_WithPayloadSnapshots_YieldsEachSnapshot()
- {
- SessionRegistry registry = new();
- FakeAlarmWorkerClient worker = new()
- {
- ReplyFactory = command =>
- {
- Assert.Equal(MxCommandKind.QueryActiveAlarms, command.Command.Kind);
- QueryActiveAlarmsReplyPayload payload = new QueryActiveAlarmsReplyPayload();
- payload.Snapshots.Add(new ActiveAlarmSnapshot
- {
- AlarmFullReference = "Galaxy!A.T1",
- CurrentState = AlarmConditionState.Active,
- Severity = 500,
- });
- payload.Snapshots.Add(new ActiveAlarmSnapshot
- {
- AlarmFullReference = "Galaxy!A.T2",
- CurrentState = AlarmConditionState.ActiveAcked,
- Severity = 100,
- });
- return new MxCommandReply
- {
- Kind = MxCommandKind.QueryActiveAlarms,
- ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
- QueryActiveAlarms = payload,
- };
- },
- };
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- List collected = new();
- await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync(
- new QueryActiveAlarmsRequest { SessionId = "s1" },
- CancellationToken.None))
- {
- collected.Add(snap);
- }
-
- Assert.Equal(2, collected.Count);
- Assert.Equal("Galaxy!A.T1", collected[0].AlarmFullReference);
- Assert.Equal("Galaxy!A.T2", collected[1].AlarmFullReference);
- }
-
- ///
- /// Server-019 regression: QueryActiveAlarmsAsync used to silently
- /// yield break when the session id was not in the registry, while the
- /// peer AcknowledgeAsync returned SessionNotFound. Both methods
- /// now signal a missing session — QueryActiveAlarms throws a
- /// with
- /// (the gateway gRPC
- /// layer maps it to gRPC NotFound), aligning the dispatcher's
- /// missing-session contract across the two RPCs.
- ///
- [Fact]
- public async Task QueryActiveAlarmsAsync_WhenSessionMissing_ThrowsSessionNotFound()
- {
- SessionRegistry registry = new();
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- SessionManagerException exception = await Assert.ThrowsAsync(async () =>
- {
- await foreach (ActiveAlarmSnapshot _ in dispatcher.QueryActiveAlarmsAsync(
- new QueryActiveAlarmsRequest { SessionId = "missing" },
- CancellationToken.None))
- {
- // No yield expected — the throw happens before the first iteration.
- }
- });
-
- Assert.Equal(SessionManagerErrorCode.SessionNotFound, exception.ErrorCode);
-
- // Peer-method parity: AcknowledgeAsync still signals SessionNotFound (as an
- // in-band ProtocolStatus, since it's a unary RPC). The two methods now agree
- // that a missing session is an error, not an empty success.
- AcknowledgeAlarmReply ackReply = await dispatcher.AcknowledgeAsync(
- new AcknowledgeAlarmRequest
- {
- SessionId = "missing",
- AlarmFullReference = Guid.NewGuid().ToString(),
- },
- CancellationToken.None);
- Assert.Equal(ProtocolStatusCode.SessionNotFound, ackReply.ProtocolStatus.Code);
- }
-
- [Fact]
- public async Task QueryActiveAlarmsAsync_WhenWorkerFails_YieldsEmpty()
- {
- SessionRegistry registry = new();
- FakeAlarmWorkerClient worker = new()
- {
- ReplyFactory = _ => new MxCommandReply
- {
- Kind = MxCommandKind.QueryActiveAlarms,
- ProtocolStatus = new ProtocolStatus
- {
- Code = ProtocolStatusCode.MxaccessFailure,
- Message = "alarm consumer not subscribed",
- },
- },
- };
- GatewaySession session = NewSession("s1");
- session.AttachWorkerClient(worker);
- session.MarkReady();
- registry.TryAdd(session);
-
- WorkerAlarmRpcDispatcher dispatcher = new(registry);
-
- List collected = new();
- await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync(
- new QueryActiveAlarmsRequest { SessionId = "s1" },
- CancellationToken.None))
- {
- collected.Add(snap);
- }
-
- Assert.Empty(collected);
- }
-
- private static GatewaySession NewSession(string sessionId)
- {
- return new(
- sessionId,
- "mxaccess",
- $"mxaccess-gateway-1-{sessionId}",
- "nonce",
- "client-1",
- "test-session",
- "client-correlation-1",
- commandTimeout: TimeSpan.FromSeconds(30),
- startupTimeout: TimeSpan.FromSeconds(5),
- shutdownTimeout: TimeSpan.FromSeconds(5),
- leaseDuration: TimeSpan.FromMinutes(30),
- openedAt: DateTimeOffset.UtcNow);
- }
-
- private sealed class FakeAlarmWorkerClient : IWorkerClient
- {
- public string SessionId { get; } = "session-1";
- public int? ProcessId { get; } = 1;
- public WorkerClientState State { get; } = WorkerClientState.Ready;
- public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
-
- public Func? ReplyFactory { get; set; }
- public int InvokeCount { get; private set; }
-
- public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- public Task InvokeAsync(
- WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken)
- {
- InvokeCount++;
- MxCommandReply reply = ReplyFactory?.Invoke(command) ?? new MxCommandReply();
- return Task.FromResult(new WorkerCommandReply { Reply = reply });
- }
-
- public async IAsyncEnumerable ReadEventsAsync(
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- await Task.CompletedTask;
- yield break;
- }
-
- public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask;
- public void Kill(string reason) { }
- public ValueTask DisposeAsync() => ValueTask.CompletedTask;
- }
-}
diff --git a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
index b741803..55e927c 100644
--- a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
+++ b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
@@ -266,7 +266,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
RpcException exception = await Assert.ThrowsAsync(
() => interceptor.UnaryServerHandler(
- new AcknowledgeAlarmRequest { SessionId = "session-1", AlarmFullReference = "ref" },
+ new AcknowledgeAlarmRequest { AlarmFullReference = "ref" },
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _) => Task.FromResult(new AcknowledgeAlarmReply())));
@@ -284,7 +284,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
bool handlerRan = false;
AcknowledgeAlarmReply reply = await interceptor.UnaryServerHandler(
- new AcknowledgeAlarmRequest { SessionId = "session-1", AlarmFullReference = "ref" },
+ new AcknowledgeAlarmRequest { AlarmFullReference = "ref" },
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _) =>
{
@@ -310,7 +310,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
RpcException exception = await Assert.ThrowsAsync(
() => interceptor.ServerStreamingServerHandler(
- new QueryActiveAlarmsRequest { SessionId = "session-1" },
+ new StreamAlarmsRequest(),
new RecordingServerStreamWriter(),
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _, _) => Task.CompletedTask));
@@ -329,7 +329,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
RecordingServerStreamWriter streamWriter = new();
await interceptor.ServerStreamingServerHandler(
- new QueryActiveAlarmsRequest { SessionId = "session-1" },
+ new StreamAlarmsRequest(),
streamWriter,
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
async (_, writer, _) =>
@@ -352,7 +352,8 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
new MxAccessGrpcMapper(),
new NoOpEventStreamService(),
new GatewayMetrics(),
- NullLogger.Instance);
+ NullLogger.Instance,
+ new FakeGatewayAlarmService());
}
private static GatewayGrpcAuthorizationInterceptor CreateInterceptor(
diff --git a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs
index 137f146..d2ad3ad 100644
--- a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs
+++ b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs
@@ -14,7 +14,7 @@ public sealed class GatewayGrpcScopeResolverTests
[InlineData(typeof(CloseSessionRequest), GatewayScopes.SessionClose)]
[InlineData(typeof(StreamEventsRequest), GatewayScopes.EventsRead)]
[InlineData(typeof(AcknowledgeAlarmRequest), GatewayScopes.InvokeWrite)]
- [InlineData(typeof(QueryActiveAlarmsRequest), GatewayScopes.EventsRead)]
+ [InlineData(typeof(StreamAlarmsRequest), GatewayScopes.EventsRead)]
[InlineData(typeof(TestConnectionRequest), GatewayScopes.MetadataRead)]
[InlineData(typeof(GetLastDeployTimeRequest), GatewayScopes.MetadataRead)]
[InlineData(typeof(DiscoverHierarchyRequest), GatewayScopes.MetadataRead)]
diff --git a/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs b/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs
new file mode 100644
index 0000000..bf32ada
--- /dev/null
+++ b/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs
@@ -0,0 +1,54 @@
+using System.Runtime.CompilerServices;
+using MxGateway.Contracts.Proto;
+using MxGateway.Server.Alarms;
+
+namespace MxGateway.Tests.TestSupport;
+
+///
+/// test double — serves a scripted
+/// active-alarm set and acknowledges every request with an OK status,
+/// so gRPC service tests can exercise the alarm handlers without the
+/// real gateway alarm monitor or a worker.
+///
+public sealed class FakeGatewayAlarmService : IGatewayAlarmService
+{
+ ///
+ public GatewayAlarmMonitorState State { get; set; } = GatewayAlarmMonitorState.Monitoring;
+
+ ///
+ public string? LastError { get; set; }
+
+ ///
+ public int? WorkerProcessId { get; set; }
+
+ ///
+ public IReadOnlyList CurrentAlarms { get; set; } = [];
+
+ ///
+ public async IAsyncEnumerable StreamAsync(
+ string? alarmFilterPrefix,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ foreach (ActiveAlarmSnapshot alarm in CurrentAlarms)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return new AlarmFeedMessage { ActiveAlarm = alarm };
+ }
+
+ yield return new AlarmFeedMessage { SnapshotComplete = true };
+ await Task.CompletedTask.ConfigureAwait(false);
+ }
+
+ ///
+ public Task AcknowledgeAsync(
+ AcknowledgeAlarmRequest request,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new AcknowledgeAlarmReply
+ {
+ CorrelationId = request.ClientCorrelationId,
+ ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
+ DiagnosticMessage = string.Empty,
+ });
+ }
+}