diff --git a/src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs b/src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs
index bbe649d..7071770 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs
@@ -19,10 +19,17 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// Declared array length when is true; null otherwise.
/// Write-authorization tier for this attribute.
/// True when this attribute is expected to feed historian / HistoryRead.
+///
+/// True when this attribute represents an alarm condition (Galaxy: has an
+/// AlarmExtension primitive). The generic node-manager enriches the variable with an
+/// OPC UA AlarmConditionState when true. Defaults to false so existing non-Galaxy
+/// drivers aren't forced to flow a flag they don't produce.
+///
public sealed record DriverAttributeInfo(
string FullName,
DriverDataType DriverDataType,
bool IsArray,
uint? ArrayDim,
SecurityClassification SecurityClass,
- bool IsHistorized);
+ bool IsHistorized,
+ bool IsAlarm = false);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/DbBackedGalaxyBackend.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/DbBackedGalaxyBackend.cs
index 9c505db..29bd850 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/DbBackedGalaxyBackend.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/DbBackedGalaxyBackend.cs
@@ -147,6 +147,7 @@ public sealed class DbBackedGalaxyBackend(GalaxyRepository repository) : IGalaxy
ArrayDim = row.ArrayDimension is int d and > 0 ? (uint)d : null,
SecurityClassification = row.SecurityClassification,
IsHistorized = row.IsHistorized,
+ IsAlarm = row.IsAlarm,
};
///
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs
index 663676d..290fa29 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs
@@ -392,6 +392,7 @@ public sealed class MxAccessGalaxyBackend : IGalaxyBackend, IDisposable
ArrayDim = row.ArrayDimension is int d and > 0 ? (uint)d : null,
SecurityClassification = row.SecurityClassification,
IsHistorized = row.IsHistorized,
+ IsAlarm = row.IsAlarm,
};
private static string MapCategory(int categoryId) => categoryId switch
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs
index 41086cb..c8749ca 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs
@@ -123,7 +123,8 @@ public sealed class GalaxyProxyDriver(GalaxyProxyOptions options)
IsArray: attr.IsArray,
ArrayDim: attr.ArrayDim,
SecurityClass: MapSecurity(attr.SecurityClassification),
- IsHistorized: attr.IsHistorized));
+ IsHistorized: attr.IsHistorized,
+ IsAlarm: attr.IsAlarm));
}
}
}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/Discovery.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/Discovery.cs
index 7ba7170..1092707 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/Discovery.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/Discovery.cs
@@ -30,6 +30,15 @@ public sealed class GalaxyAttributeInfo
[Key(3)] public uint? ArrayDim { get; set; }
[Key(4)] public int SecurityClassification { get; set; }
[Key(5)] public bool IsHistorized { get; set; }
+
+ ///
+ /// True when the attribute has an AlarmExtension primitive in the Galaxy repository
+ /// (primitive_definition.primitive_name = 'AlarmExtension'). The generic
+ /// node-manager uses this to enrich the variable's OPC UA node with an
+ /// AlarmConditionState during address-space build. Added in PR 9 as the
+ /// discovery-side foundation for the alarm event wire-up that follows in PR 10+.
+ ///
+ [Key(6)] public bool IsAlarm { get; set; }
}
[MessagePackObject]
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/AlarmDiscoveryTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/AlarmDiscoveryTests.cs
new file mode 100644
index 0000000..27541ab
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/AlarmDiscoveryTests.cs
@@ -0,0 +1,84 @@
+using System;
+using MessagePack;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class AlarmDiscoveryTests
+{
+ ///
+ /// PR 9 — IsAlarm must survive the MessagePack round-trip at Key=6 position.
+ /// Regression guard: any reorder of keys in GalaxyAttributeInfo would silently corrupt
+ /// the flag in the wire payload since MessagePack encodes by key number, not field name.
+ ///
+ [Fact]
+ public void GalaxyAttributeInfo_IsAlarm_round_trips_true_through_MessagePack()
+ {
+ var input = new GalaxyAttributeInfo
+ {
+ AttributeName = "TankLevel",
+ MxDataType = 2,
+ IsArray = false,
+ ArrayDim = null,
+ SecurityClassification = 1,
+ IsHistorized = true,
+ IsAlarm = true,
+ };
+
+ var bytes = MessagePackSerializer.Serialize(input);
+ var decoded = MessagePackSerializer.Deserialize(bytes);
+
+ decoded.IsAlarm.ShouldBeTrue();
+ decoded.IsHistorized.ShouldBeTrue();
+ decoded.AttributeName.ShouldBe("TankLevel");
+ }
+
+ [Fact]
+ public void GalaxyAttributeInfo_IsAlarm_round_trips_false_through_MessagePack()
+ {
+ var input = new GalaxyAttributeInfo { AttributeName = "ColorRgb", IsAlarm = false };
+ var bytes = MessagePackSerializer.Serialize(input);
+ var decoded = MessagePackSerializer.Deserialize(bytes);
+ decoded.IsAlarm.ShouldBeFalse();
+ }
+
+ ///
+ /// Wire-compat guard: payloads serialized before PR 9 (which omit Key=6) must still
+ /// deserialize cleanly — MessagePack treats missing keys as default. This lets a newer
+ /// Proxy talk to an older Host during a rolling upgrade without a crash.
+ ///
+ [Fact]
+ public void Pre_PR9_payload_without_IsAlarm_key_deserializes_with_default_false()
+ {
+ // Build a 6-field payload (keys 0..5) matching the pre-PR9 shape by serializing a
+ // stand-in class with the same key layout but no Key=6.
+ var pre = new PrePR9Shape
+ {
+ AttributeName = "Legacy",
+ MxDataType = 1,
+ IsArray = false,
+ ArrayDim = null,
+ SecurityClassification = 0,
+ IsHistorized = false,
+ };
+ var bytes = MessagePackSerializer.Serialize(pre);
+
+ var decoded = MessagePackSerializer.Deserialize(bytes);
+ decoded.AttributeName.ShouldBe("Legacy");
+ decoded.IsAlarm.ShouldBeFalse();
+ }
+
+ [MessagePackObject]
+ public sealed class PrePR9Shape
+ {
+ [Key(0)] public string AttributeName { get; set; } = string.Empty;
+ [Key(1)] public int MxDataType { get; set; }
+ [Key(2)] public bool IsArray { get; set; }
+ [Key(3)] public uint? ArrayDim { get; set; }
+ [Key(4)] public int SecurityClassification { get; set; }
+ [Key(5)] public bool IsHistorized { get; set; }
+ }
+}