#region Using directives
using FTOptix.Alarm;
using FTOptix.Core;
using FTOptix.HMIProject;
using FTOptix.NetLogic;
using FTOptix.UI;
using UAManagedCore;
using OpcUa = UAManagedCore.OpcUa;
#endregion
public class RuntimeAlarmsLogic : BaseNetLogic
{
public override void Start()
{
// Insert code to be run when the user-defined logic is started
DetectAlarmType();
}
public override void Stop()
{
// Insert code to be run when the user-defined logic is stopped
}
[ExportMethod]
public void DetectAlarmType()
{
// Check which alarm type we need to create and display the related info on a Label
var targetLabel = Owner.Get<Label>("AlarmType");
var alarmTag = InformationModel.GetVariable(Owner.Get<ComboBox>("SourceVar").SelectedItem);
if (alarmTag.DataType == OpcUa.DataTypes.Boolean)
{
targetLabel.LocalizedText = new LocalizedText(targetLabel.NodeId.NamespaceIndex, "DigitalAlarm");
}
else
{
targetLabel.LocalizedText = new LocalizedText(targetLabel.NodeId.NamespaceIndex, "AnalogAlarm");
}
}
[ExportMethod]
public void CreateAlarm()
{
// Get the alarms container
var alarmsFolder = Project.Current.Get<Folder>("Alarms/RuntimeAlarms");
var alarmTag = InformationModel.GetVariable(Owner.Get<ComboBox>("SourceVar").SelectedItem);
var existingAlarm = alarmsFolder.Get(alarmTag.BrowseName) ?? null;
// Check if alarm already exist (skip) or create new based on DataType
if (existingAlarm == null)
{
if (alarmTag.DataType == OpcUa.DataTypes.Boolean)
{
var newAlarm = InformationModel.MakeObject<DigitalAlarm>(alarmTag.BrowseName);
newAlarm.InputValueVariable.SetDynamicLink(alarmTag);
newAlarm.Message = Owner.Get<TextBox>("AlarmMessage").Text;
newAlarm.AutoAcknowledge = true;
newAlarm.AutoConfirm = true;
alarmsFolder.Add(newAlarm);
}
else
{
var newAlarm = InformationModel.MakeObject<ExclusiveLevelAlarmController>(alarmTag.BrowseName);
newAlarm.InputValueVariable.SetDynamicLink(alarmTag);
newAlarm.Message = Owner.Get<TextBox>("AlarmMessage").Text;
newAlarm.HighLimit = 50;
newAlarm.AutoAcknowledge = true;
newAlarm.AutoConfirm = true;
alarmsFolder.Add(newAlarm);
}
}
}
[ExportMethod]
public void DeleteAlarm()
{
// Delete alarm if exist
var alarmsFolder = Project.Current.Get<Folder>("Alarms/RuntimeAlarms");
var alarmTag = InformationModel.GetVariable(Owner.Get<ComboBox>("SourceVar").SelectedItem);
var existingAlarm = alarmsFolder.Get(alarmTag.BrowseName) ?? null;
existingAlarm?.Delete();
}
}#region Using directives
using System;
using UAManagedCore;
using OpcUa = UAManagedCore.OpcUa;
using FTOptix.UI;
using FTOptix.NativeUI;
using FTOptix.HMIProject;
using FTOptix.Retentivity;
using FTOptix.CoreBase;
using FTOptix.Core;
using FTOptix.NetLogic;
using FTOptix.Alarm;
using System.Collections.Generic;
using LogsHandler;
using System.Transactions;
#endregion
public class AlarmsObserverLogic : BaseNetLogic
{
public override void Start()
{
// Get the affinity ID of the current NetLogic to avoid cross thread issues
affinityId = LogicObject.Context.AssignAffinityId();
// Create the observer to check when alarms are created and/or deleted
StartObserver();
}
public override void Stop()
{
// Destroy the observer when closing the NetLogic
alarmsCreationObserver?.Dispose();
}
public void StartObserver()
{
// Get the alarms server object
var retainedAlarmsObject = LogicObject.Context.GetNode(FTOptix.Alarm.Objects.RetainedAlarms);
// Get the object containing the actual list of alarms
var localizedAlarmsObject = retainedAlarmsObject.GetVariable("LocalizedAlarms");
var localizedAlarmsNodeId = (NodeId) localizedAlarmsObject.Value;
IUANode localizedAlarmsContainer = null;
if (localizedAlarmsNodeId?.IsEmpty == false)
localizedAlarmsContainer = LogicObject.Context.GetNode(localizedAlarmsNodeId);
if (localizedAlarmsContainer == null)
{
Log.Error("AlarmsObserverLogic", "LocalizedAlarms node not found");
return;
}
// Create a new custom alarms observer
var logsObserver = new AlarmsCreationObserver();
// Register the observer to the server node
alarmsCreationObserver = localizedAlarmsContainer.RegisterEventObserver(
logsObserver, // Which observer to use
EventType.ForwardReferenceAdded | // Register when alarms are created
EventType.ForwardReferenceRemoved, // Register when alarms are disposed
affinityId); // Pass the affinity ID
}
private IEventRegistration alarmsCreationObserver;
private uint affinityId; // This can also be moved to a local variable
}
namespace LogsHandler
{
class AlarmsCreationObserver : IReferenceObserver
{
public AlarmsCreationObserver()
{
// Just create the class instance, nothing really to do
Log.Info("LogsEventObserver", "Starting alarms observer");
}
public void OnReferenceAdded(IUANode sourceNode, IUANode targetNode, NodeId referenceTypeId, ulong senderId)
{
// When an alarm gets activated it will be added as reference to the RetainedAlarms object
// Here a custom logic can be added to trigger some actions
Log.Info("LogsEventObserver", $"{targetNode.BrowseName} alarm got enabled");
// Subscribe to some variables (can be customized as needed)
try
{
if (targetNode is not UAObject alarmNode)
{
Log.Error("LogsEventObserver", "Alarm node is invalid");
return;
}
// Trigger the VariableChange to some variables, can be customized to call different methods if needed
alarmNode.GetVariable("ActiveState/Id").VariableChange += AlarmsCreationObserver_VariableChange;
alarmNode.GetVariable("AckedState/Id").VariableChange += AlarmsCreationObserver_VariableChange;
alarmNode.GetVariable("ConfirmedState/Id").VariableChange += AlarmsCreationObserver_VariableChange;
}
catch
{
Log.Error("LogsEventObserver", "Error while trying to subscribe to alarm variables");
}
}
// Method to be called whenever a variable changes in value
private void AlarmsCreationObserver_VariableChange(object sender, VariableChangeEventArgs e)
{
// Do something when variables are changing
var variable = sender as IUAVariable;
Log.Info("LogsEventObserver", $"{variable.Owner.BrowseName} alarm status changed to {e.NewValue}");
}
public void OnReferenceRemoved(IUANode sourceNode, IUANode targetNode, NodeId referenceTypeId, ulong senderId)
{
// Do something when the alarms deactivates
Log.Info("LogsEventObserver", $"{targetNode.BrowseName} alarm got removed");
}
}
}/// <summary>
/// Retrieves the <see cref="AlarmController"/> associated with the given retained alarm ID.
/// </summary>
/// <param name="retainedAlarmId">The <see cref="NodeId"/> of the alarm in the RetainedAlarm object.</param>
/// <returns>The <see cref="AlarmController"/> associated with the retained alarm.</returns>
/// <exception cref="System.ArgumentException">Thrown when the alarm is not found.</exception>
private static AlarmController GetAlarmFromRetainedAlarm(NodeId retainedAlarmId)
{
// Get the alarm controller from the retained alarm
var retainedAlarm = InformationModel.Get(retainedAlarmId);
// Get the alarm controller from the retained alarm
return InformationModel.Get<AlarmController>(retainedAlarm.GetVariable("ConditionId").Value) ?? throw new System.ArgumentException("Alarm not found");
}[ExportMethod]
public void PrintSourceVariableOfAllAlarms()
{
// Get the RetainedAlarms server object
var retainedAlarmsObject = LogicObject.Context.GetNode(FTOptix.Alarm.Objects.RetainedAlarms);
// Get the node that holds the actual list of active/retained alarms
var localizedAlarmsObject = retainedAlarmsObject.GetVariable("LocalizedAlarms");
var localizedAlarmsNodeId = (NodeId)localizedAlarmsObject.Value;
IUANode localizedAlarmsContainer = null;
if (localizedAlarmsNodeId?.IsEmpty == false)
localizedAlarmsContainer = LogicObject.Context.GetNode(localizedAlarmsNodeId);
if (localizedAlarmsContainer == null)
{
Log.Error("PrintSourceVariableOfAllAlarms", "LocalizedAlarms node not found");
return;
}
// Iterate over every active retained alarm
foreach (var item in localizedAlarmsContainer.Children)
{
// Resolve the retained alarm back to its originating AlarmController
var conditionIdVar = item.GetVariable("ConditionId");
if (conditionIdVar == null)
continue;
// Get the source variable name for logging purposes
string sourceVariableName = item.GetVariable("SourceName").Value;
// Get the AlarmController node using the ConditionId
var alarmController = InformationModel.Get<AlarmController>((NodeId)conditionIdVar.Value);
if (alarmController == null)
continue;
// Get the source variable of the alarm by following the dynamic link from the AlarmController's InputValueVariable
var sourceVariableDynamicLink = alarmController.InputValueVariable.Refs.GetVariable(FTOptix.CoreBase.ReferenceTypes.HasDynamicLink);
var pointedNode = sourceVariableDynamicLink.Refs.GetNodes(FTOptix.Core.ReferenceTypes.Resolves).FirstOrDefault();
if (pointedNode == null)
{
Log.Error("PrintSourceVariableOfAllAlarms", $"No source variable linked to AlarmController: {alarmController.BrowseName}");
continue;
}
// Print the current value of the alarm's input variable
Log.Info("PrintSourceVariableOfAllAlarms",
$"Alarm: {alarmController.BrowseName}, InputVariableName: {sourceVariableName} ({Log.Node(pointedNode)}), InputVariable value: {alarmController.InputValueVariable.Value}");
}
}Warning
The multi selection feature can only be used in the AlarmGrid component. The standard DataGrid object does not support multi selection and forcing it may cause unexpected results.
public class AlarmGridLogic : BaseNetLogic
{
public override void Start()
{
alarmsDataGridModel = Owner.Get<DataGrid>("AlarmsDataGrid").GetVariable("Model");
}
/// <summary>
/// Acknowledges the selected alarms with a specified message.
/// </summary>
/// <param name="ackMessage">The acknowledgment message.</param>
[ExportMethod]
public void AckAlarmsWithMessage(LocalizedText ackMessage)
{
ProcessAlarms(ackMessage, (alarm, message) => alarm.Acknowledge(message));
}
/// <summary>
/// Confirms the selected alarms with a specified message.
/// </summary>
/// <param name="confirmMessage">The confirmation message.</param>
[ExportMethod]
public void ConfirmAlarmsWithMessage(LocalizedText confirmMessage)
{
ProcessAlarms(confirmMessage, (alarm, message) => alarm.Confirm(message));
}
#region Private methods
/// <summary>
/// Processes the selected alarms with a specified action.
/// </summary>
/// <param name="message">The message to be used for the action.</param>
/// <param name="alarmAction">The action to be performed on the alarms.</param>
private void ProcessAlarms(LocalizedText message, Action<AlarmController, LocalizedText> alarmAction)
{
var dataGrid = Owner.Get<DataGrid>("AlarmsDataGrid");
if (dataGrid.GetVariable("AllowMultiSelection").Value)
{
// Multi selection
var selectedItemsNodes = dataGrid.GetOptionalVariableValue("UISelectedItems") ?? throw new System.ArgumentException("UISelectedItems variable not found in AlarmsDataGrid");
var selectedItemsArray = (NodeId[]) selectedItemsNodes.Value;
if (selectedItemsArray == null || selectedItemsArray.Length == 0)
{
throw new System.ArgumentException("No alarms selected");
}
// Process each selected alarm
foreach (var nodeId in selectedItemsArray)
{
var alarm = GetAlarmFromRetainedAlarm(nodeId) ?? throw new System.ArgumentException("Alarm not found");
alarmAction(alarm, message);
}
}
else
{
// Single selection
var alarm = GetAlarmFromRetainedAlarm(dataGrid.UISelectedItem) ?? throw new System.ArgumentException("Alarm not found");
alarmAction(alarm, message);
}
}
/// <summary>
/// Retrieves the <see cref="AlarmController"/> associated with the given retained alarm ID.
/// </summary>
/// <param name="retainedAlarmId">The <see cref="NodeId"/> of the retained alarm.</param>
/// <returns>The <see cref="AlarmController"/> associated with the retained alarm.</returns>
/// <exception cref="System.ArgumentException">Thrown when the alarm is not found.</exception>
private static AlarmController GetAlarmFromRetainedAlarm(NodeId retainedAlarmId)
{
// Get the alarm controller from the retained alarm
var retainedAlarm = InformationModel.Get(retainedAlarmId);
// Get the alarm controller from the retained alarm
return InformationModel.Get<AlarmController>(retainedAlarm.GetVariable("ConditionId").Value) ?? throw new System.ArgumentException("Alarm not found");
}
#endregion
#endregion
private IUAVariable alarmsDataGridModel;
}/// <summary>
/// Run the AlarmCommands object methods to acknowledge and confirm all active alarms.
/// This is a global action and requires proper permissions.
/// </summary>
var alarmCommands = InformationModel.GetObject(FTOptix.Alarm.Objects.AlarmCommands);
alarmCommands.ExecuteMethod("AcknowledgeAll");
alarmCommands.ExecuteMethod("ConfirmAll");/// <summary>
/// Write to the log the list of retained (active) localized alarms.
/// </summary>
[ExportMethod]
public void LogActiveAlarms()
{
var retainedAlarmsObject = LogicObject.Context.GetNode(FTOptix.Alarm.Objects.RetainedAlarms);
// Get the object containing the actual list of alarms
var localizedAlarmsObject = retainedAlarmsObject.GetVariable("LocalizedAlarms");
var localizedAlarmsNodeId = (NodeId)localizedAlarmsObject.Value;
IUANode localizedAlarmsContainer = null;
if (localizedAlarmsNodeId?.IsEmpty == false)
localizedAlarmsContainer = LogicObject.Context.GetNode(localizedAlarmsNodeId);
if (localizedAlarmsContainer == null)
{
Log.Error("AlarmsObserverLogic", "LocalizedAlarms node not found");
return;
}
foreach (var item in localizedAlarmsContainer.Children)
{
Log.Info(item.BrowseName);
}
}Warning
This helper will create a large number of alarms (one per bit per array element). Use carefully and prefer to run at DesignTime when possible.
/// <summary>
/// Create digital alarms for each bit of an array of integer/word values.
/// Input: a NodeId variable "VarArrayPLC" pointing to the PLC array variable.
/// Behavior: the method creates a folder under Alarms and an alarm for each bit found in the array.
/// </summary>
[ExportMethod]
public void CreateBitAlarms()
{
var plc = InformationModel.GetVariable(LogicObject.GetVariable("VarArrayPLC").Value);
var dataTypeBrowse = InformationModel.Get(plc.DataType).BrowseName;
int numBits = dataTypeBrowse switch
{
"Int32" => 32,
"UInt32" => 32,
"Int16" => 16,
"UInt16" => 16,
_ => 0
};
if (numBits == 0)
{
Log.Error("CreateBitAlarms", "Unsupported data type: " + dataTypeBrowse);
return;
}
var newFolder = InformationModel.Make<Folder>(plc.BrowseName);
Project.Current.Get("Alarms").Add(newFolder);
// Assume plc is a UAVariable array; iterate over elements and bits
var arrayLength = ((UAManagedCore.UAVariable)plc).ArrayDimensions[0];
for (int i = 1; i < arrayLength; i++)
{
for (int j = 0; j < numBits; j++)
{
var alarm = InformationModel.Make<DigitalAlarm>($"Alarm_{plc.BrowseName}_{i}_{j}");
// Create a dynamic link to the element and then point to the bit index
alarm.InputValueVariable.SetDynamicLink((IUAVariable)plc, (uint)i, DynamicLinkMode.ReadWrite);
// Resolve the DynamicLink variable using the HasDynamicLink reference (recommended)
var dynamicLinkVar = alarm.InputValueVariable.Refs.GetVariable(FTOptix.CoreBase.ReferenceTypes.HasDynamicLink) as IUAVariable;
if (dynamicLinkVar != null)
{
dynamicLinkVar.Value = (string)dynamicLinkVar.Value + "." + j;
}
alarm.Message = $"Alarm_{plc.BrowseName}_{i}_{j}";
Project.Current.Get($"Alarms/{plc.BrowseName}").Add(alarm);
}
}
}/// <summary>
/// Build a LocalizedText entry for an alarm and add it to the InformationModel translations if missing.
/// </summary>
private void AddLocalizedMessageToAlarm(AlarmController alarmInstance)
{
var alarm = (DigitalAlarm)item;
var localizedMessage = new LocalizedText(alarmInstance.BrowseName + "_" + item.BrowseName, alarmInstance.BrowseName + "_" + item.BrowseName, "en-US");
if (!InformationModel.LookupTranslation(localizedMessage).HasTranslation)
{
InformationModel.AddTranslation(localizedMessage);
}
alarm.LocalizedMessage = localizedMessage;
}