/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using Microsoft.Extensions.Logging; using Opc.Ua; namespace Alarms { /// /// Alarm controller /// public class AlarmController { private const int kDefaultCycleTime = 180; protected ILogger m_logger; protected BaseDataVariableState m_variable; protected int m_value; protected bool m_increment = true; protected DateTime m_nextTime = DateTime.Now; protected DateTime m_stopTime = DateTime.Now; protected int m_interval; protected bool m_isBoolean; protected bool m_allowChanges; protected bool m_reset; protected DateTime m_lastMaxValue; protected bool m_validLastMaxValue; protected int m_midpoint = AlarmDefines.NORMAL_START_VALUE; public AlarmController(BaseDataVariableState variable, int interval, bool isBoolean, ITelemetryContext telemetry) { m_logger = telemetry.CreateLogger(); m_variable = variable; m_interval = interval; m_isBoolean = isBoolean; m_increment = true; m_value = m_midpoint; m_stopTime = m_stopTime.AddYears(5); m_allowChanges = false; } /// /// Start the Alarm cycles for n seconds. /// public virtual void Start(uint seconds = 0) { Stop(); m_logger.LogInformation("Start the Alarms for {Duration} seconds!", seconds); m_validLastMaxValue = false; m_nextTime = m_stopTime = DateTime.Now; m_stopTime = m_stopTime.AddSeconds(seconds == 0 ? kDefaultCycleTime : seconds); m_allowChanges = true; } public virtual void Stop() { m_logger.LogInformation("Stop the Alarms!"); m_value = m_midpoint; m_increment = true; m_allowChanges = false; m_reset = true; } public virtual bool Update(ISystemContext systemContext) { bool valueSet = false; if (CanSetValue()) { int value = 0; bool boolValue = false; GetValue(ref value, ref boolValue); m_logger.LogInformation("AlarmController Update Value = {Value}", value); if (m_isBoolean) { m_variable.Value = boolValue; } else { m_variable.Value = value; } m_variable.Timestamp = DateTime.UtcNow; m_variable.ClearChangeMasks(systemContext, false); valueSet = true; } return valueSet; } protected virtual void SetNextInterval() { m_nextTime = DateTime.Now; m_nextTime = m_nextTime.AddMilliseconds(m_interval); } public void ManualWrite(object value) { if (value.GetType().Name == "Int32") { // Don't let anyone write a value out of range int potentialWrite = (int)value; if (potentialWrite is >= AlarmDefines.MIN_VALUE and <= AlarmDefines.MAX_VALUE) { m_value = potentialWrite; } else { m_logger.LogError( "AlarmController Received out of range manual write of {Value}", value); } } else if ((bool)value) { m_value = 70; m_increment = true; } else { m_value = m_midpoint; } } public virtual bool CanSetValue() { bool setValue = false; if (DateTime.Now > m_stopTime) { Stop(); m_stopTime = DateTime.MaxValue; } else if (m_allowChanges || m_reset) { m_reset = false; if (DateTime.Now > m_nextTime) { SetNextInterval(); setValue = true; } } return setValue; } protected virtual void GetValue(ref int intValue, ref bool boolValue) { const int maxValue = 100; const int minValue = 0; TypicalGetValue(minValue, maxValue, ref intValue, ref boolValue); } public bool SupportsBranching { get; set; } public virtual void SetBranchCount(int count) { } protected void TypicalGetValue( int minValue, int maxValue, ref int intValue, ref bool boolValue) { int incrementValue = 5; if (m_isBoolean) { incrementValue = 10; } if (m_increment) { m_value += incrementValue; if (m_value >= maxValue) { if (m_validLastMaxValue) { m_logger.LogInformation( "Cycle Time {CycleTime} Interval {Interval}", DateTime.Now - m_lastMaxValue, m_interval); } m_lastMaxValue = DateTime.Now; m_validLastMaxValue = true; m_increment = false; } } else { m_value -= incrementValue; if (m_value <= minValue) { m_increment = true; } } intValue = m_value; boolValue = IsBooleanActive(); } public bool IsBooleanActive() { bool isActive = false; if (m_value is >= AlarmDefines.BOOL_HIGH_ALARM or <= AlarmDefines.BOOL_LOW_ALARM) { isActive = true; } return isActive; } public int GetValue() { return m_value; } public int GetSawtooth() { return m_value; } public int GetSine(int minValue, int maxValue) { return CalcSine(minValue, maxValue, m_value); } public int CalcSine(int minValue, int maxValue, int value) { // What I want is a sawtooth compared against a sine value. // This calculates a simular sine value that will have predictable differences between value and sine /* * https://www.mathsisfun.com/algebra/amplitude-period-frequency-phase-shift.html * Sine with Phase Shift and Vertical Shift! This is what I want * y = A sin(B(x + C)) + D * A - Amplitude * B - relates to period - This should extend the time period * C - Phase Shift * D - Vertical Shift * */ const double twoPi = Math.PI * 2; double normalSpan = maxValue - minValue; double amplitude = normalSpan / 2; double median = maxValue - amplitude; double offsetValue = value - minValue; double percentageOfRange = offsetValue / normalSpan; double reducedPeriod = percentageOfRange / 2; const double period = twoPi; // this would relate to the interval. Ignore for now. const double phase = -0.25; // phaseShift; double verticalShift = median; // amplitude double calculated = (amplitude * Math.Sin(period * (reducedPeriod + phase))) + verticalShift; m_logger.LogTrace( " Phase {Phase:0.00} Value {Value} Sine {Sine:0.00}" + " Offset Value {OffsetValue:0.00} Span {NormalSpan:0.00}" + " Percentage of Range {PercentageOfRange:0.00}", phase, value, calculated, offsetValue, normalSpan, percentageOfRange); return (int)calculated; } public virtual bool ShouldSuppress() { return false; } public virtual bool ShouldUnsuppress() { return false; } public virtual void OnAddComment() { } public virtual void OnAcknowledge() { } public virtual void OnConfirm() { } } }