///|
/// Severity assigned to an operational alarm.
pub(all) enum AlarmSeverity {
Informational
Warning
Critical
} derive(Debug, Eq)
///|
/// Comparison used by an alarm rule.
pub(all) enum AlarmCondition {
Below
AtOrBelow
Equal
NotEqual
AtOrAbove
Above
Outside
Inside
} derive(Debug, Eq)
///|
/// Lifecycle state of an alarm instance.
pub(all) enum AlarmState {
Normal
Pending
Active
Acknowledged
Clearing
Shelved
} derive(Debug, Eq)
///|
pub fn alarm_severity_name(value : AlarmSeverity) -> String {
match value {
Informational => "informational"
Warning => "warning"
Critical => "critical"
}
}
///|
pub fn alarm_condition_name(value : AlarmCondition) -> String {
match value {
Below => "below"
AtOrBelow => "at-or-below"
Equal => "equal"
NotEqual => "not-equal"
AtOrAbove => "at-or-above"
Above => "above"
Outside => "outside"
Inside => "inside"
}
}
///|
pub fn alarm_state_name(value : AlarmState) -> String {
match value {
Normal => "normal"
Pending => "pending"
Active => "active"
Acknowledged => "acknowledged"
Clearing => "clearing"
Shelved => "shelved"
}
}
///|
/// A declarative threshold rule attached to a profile point.
pub(all) struct AlarmRule {
id : String
point_id : String
condition : AlarmCondition
low : Int
high : Int
severity : AlarmSeverity
activation_delay : Int
clear_delay : Int
hysteresis : Int
mut enabled : Bool
}
///|
pub fn AlarmRule::new(
id : String,
point_id : String,
condition : AlarmCondition,
low : Int,
high : Int,
severity : AlarmSeverity,
activation_delay? : Int = 0,
clear_delay? : Int = 0,
hysteresis? : Int = 0,
) -> Result[AlarmRule, ModbusError] {
if id == "" || point_id == "" || low > high {
return Err(InvalidData)
}
if activation_delay < 0 || clear_delay < 0 || hysteresis < 0 {
return Err(InvalidData)
}
Ok({
id,
point_id,
condition,
low,
high,
severity,
activation_delay,
clear_delay,
hysteresis,
enabled: true,
})
}
///|
pub fn AlarmRule::id(self : AlarmRule) -> String {
self.id
}
///|
pub fn AlarmRule::point_id(self : AlarmRule) -> String {
self.point_id
}
///|
pub fn AlarmRule::condition(self : AlarmRule) -> AlarmCondition {
self.condition
}
///|
pub fn AlarmRule::low(self : AlarmRule) -> Int {
self.low
}
///|
pub fn AlarmRule::high(self : AlarmRule) -> Int {
self.high
}
///|
pub fn AlarmRule::severity(self : AlarmRule) -> AlarmSeverity {
self.severity
}
///|
pub fn AlarmRule::activation_delay(self : AlarmRule) -> Int {
self.activation_delay
}
///|
pub fn AlarmRule::clear_delay(self : AlarmRule) -> Int {
self.clear_delay
}
///|
pub fn AlarmRule::hysteresis(self : AlarmRule) -> Int {
self.hysteresis
}
///|
pub fn AlarmRule::enabled(self : AlarmRule) -> Bool {
self.enabled
}
///|
pub fn AlarmRule::set_enabled(self : AlarmRule, enabled : Bool) -> Unit {
self.enabled = enabled
}
///|
pub fn AlarmRule::matches(self : AlarmRule, value : Int) -> Bool {
if !self.enabled {
return false
}
match self.condition {
Below => value < self.low
AtOrBelow => value <= self.low
Equal => value == self.low
NotEqual => value != self.low
AtOrAbove => value >= self.high
Above => value > self.high
Outside => value < self.low || value > self.high
Inside => value > self.low && value < self.high
}
}
///|
pub fn AlarmRule::clear_matches(self : AlarmRule, value : Int) -> Bool {
if !self.enabled {
return true
}
let low = self.low + self.hysteresis
let high = self.high - self.hysteresis
match self.condition {
Below | AtOrBelow => value >= low
Equal => value != self.low
NotEqual => value == self.low
AtOrAbove | Above => value <= high
Outside => value >= low && value <= high
Inside => value <= low || value >= high
}
}
///|
pub fn AlarmRule::summary(self : AlarmRule) -> String {
self.id +
" point=" +
self.point_id +
" condition=" +
alarm_condition_name(self.condition) +
" low=" +
self.low.to_string() +
" high=" +
self.high.to_string() +
" severity=" +
alarm_severity_name(self.severity)
}
///|
/// An event emitted when an alarm lifecycle changes.
pub(all) struct AlarmEvent {
sequence : Int
rule_id : String
point_id : String
state : AlarmState
value : Int
timestamp : Int
severity : AlarmSeverity
message : String
} derive(Debug)
///|
pub fn AlarmEvent::sequence(self : AlarmEvent) -> Int {
self.sequence
}
///|
pub fn AlarmEvent::rule_id(self : AlarmEvent) -> String {
self.rule_id
}
///|
pub fn AlarmEvent::point_id(self : AlarmEvent) -> String {
self.point_id
}
///|
pub fn AlarmEvent::state(self : AlarmEvent) -> AlarmState {
self.state
}
///|
pub fn AlarmEvent::value(self : AlarmEvent) -> Int {
self.value
}
///|
pub fn AlarmEvent::timestamp(self : AlarmEvent) -> Int {
self.timestamp
}
///|
pub fn AlarmEvent::severity(self : AlarmEvent) -> AlarmSeverity {
self.severity
}
///|
pub fn AlarmEvent::message(self : AlarmEvent) -> String {
self.message
}
///|
pub fn AlarmEvent::summary(self : AlarmEvent) -> String {
self.rule_id +
" " +
alarm_state_name(self.state) +
" value=" +
self.value.to_string() +
" at=" +
self.timestamp.to_string()
}
///|
/// Internal state retained for one rule.
pub(all) struct AlarmInstance {
rule : AlarmRule
mut state : AlarmState
mut pending_since : Int?
mut clearing_since : Int?
mut last_value : Int
mut last_timestamp : Int
mut transitions : Int
mut acknowledger : String?
}
///|
pub fn AlarmInstance::new(rule : AlarmRule) -> AlarmInstance {
{
rule,
state: Normal,
pending_since: None,
clearing_since: None,
last_value: 0,
last_timestamp: 0,
transitions: 0,
acknowledger: None,
}
}
///|
pub fn AlarmInstance::rule(self : AlarmInstance) -> AlarmRule {
self.rule
}
///|
pub fn AlarmInstance::id(self : AlarmInstance) -> String {
self.rule.id()
}
///|
pub fn AlarmInstance::point_id(self : AlarmInstance) -> String {
self.rule.point_id()
}
///|
pub fn AlarmInstance::state(self : AlarmInstance) -> AlarmState {
self.state
}
///|
pub fn AlarmInstance::last_value(self : AlarmInstance) -> Int {
self.last_value
}
///|
pub fn AlarmInstance::last_timestamp(self : AlarmInstance) -> Int {
self.last_timestamp
}
///|
pub fn AlarmInstance::transitions(self : AlarmInstance) -> Int {
self.transitions
}
///|
pub fn AlarmInstance::acknowledger(self : AlarmInstance) -> String? {
self.acknowledger
}
///|
pub fn AlarmInstance::is_active(self : AlarmInstance) -> Bool {
self.state == Active || self.state == Acknowledged || self.state == Clearing
}
///|
pub fn AlarmInstance::acknowledge(
self : AlarmInstance,
operator : String,
) -> Result[Unit, ModbusError] {
if operator == "" {
return Err(InvalidData)
}
if self.state == Active {
self.state = Acknowledged
self.acknowledger = Some(operator)
self.transitions += 1
Ok(())
} else if self.state == Acknowledged {
self.acknowledger = Some(operator)
Ok(())
} else {
Err(InvalidData)
}
}
///|
pub fn AlarmInstance::shelve(self : AlarmInstance) -> Result[Unit, ModbusError] {
if self.state == Normal {
Err(InvalidData)
} else {
self.state = Shelved
self.transitions += 1
Ok(())
}
}
///|
pub fn AlarmInstance::unshelve(self : AlarmInstance) -> Unit {
if self.state == Shelved {
self.state = Normal
self.pending_since = None
self.clearing_since = None
self.acknowledger = None
self.transitions += 1
}
}
///|
/// A deterministic transition result used by polling and UI layers.
pub(all) enum AlarmTransition {
Unchanged(AlarmState)
Changed(AlarmEvent)
} derive(Debug)
///|
fn alarm_event(
instance : AlarmInstance,
state : AlarmState,
value : Int,
timestamp : Int,
sequence : Int,
) -> AlarmEvent {
{
sequence,
rule_id: instance.id(),
point_id: instance.point_id(),
state,
value,
timestamp,
severity: instance.rule().severity(),
message: instance.id() + " -> " + alarm_state_name(state),
}
}
///|
fn alarm_emit_transition(
instance : AlarmInstance,
old : AlarmState,
value : Int,
timestamp : Int,
sequence : Int,
) -> AlarmTransition {
if old == instance.state() {
Unchanged(old)
} else {
Changed(alarm_event(instance, instance.state(), value, timestamp, sequence))
}
}
///|
/// Evaluate one new sample and update the instance lifecycle.
pub fn AlarmInstance::evaluate(
self : AlarmInstance,
value : Int,
timestamp : Int,
sequence : Int,
) -> AlarmTransition {
let old = self.state
if timestamp < self.last_timestamp {
return Unchanged(self.state)
}
self.last_value = value
self.last_timestamp = timestamp
if self.state == Shelved {
return Unchanged(self.state)
}
if !self.rule.enabled() {
self.state = Normal
self.pending_since = None
self.clearing_since = None
return alarm_emit_transition(self, old, value, timestamp, sequence)
}
match self.state {
Normal =>
if self.rule.matches(value) {
if self.rule.activation_delay() == 0 {
self.state = Active
self.transitions += 1
} else {
self.state = Pending
self.pending_since = Some(timestamp)
self.transitions += 1
}
}
Pending =>
if !self.rule.matches(value) {
self.state = Normal
self.pending_since = None
self.transitions += 1
} else if self.pending_since is Some(start) &&
timestamp - start >= self.rule.activation_delay() {
self.state = Active
self.pending_since = None
self.transitions += 1
}
Active | Acknowledged =>
if self.rule.clear_matches(value) {
if self.rule.clear_delay() == 0 {
self.state = Normal
self.acknowledger = None
self.transitions += 1
} else {
self.state = Clearing
self.clearing_since = Some(timestamp)
self.transitions += 1
}
}
Clearing =>
if !self.rule.clear_matches(value) {
self.state = Active
self.clearing_since = None
self.transitions += 1
} else if self.clearing_since is Some(start) &&
timestamp - start >= self.rule.clear_delay() {
self.state = Normal
self.clearing_since = None
self.acknowledger = None
self.transitions += 1
}
Shelved => ()
}
alarm_emit_transition(self, old, value, timestamp, sequence)
}
///|
/// A bounded alarm engine suitable for a device server or polling loop.
pub(all) struct AlarmEngine {
instances : Array[AlarmInstance]
events : Array[AlarmEvent]
max_events : Int
mut next_sequence : Int
}
///|
pub fn AlarmEngine::new(
max_events? : Int = 2048,
) -> Result[AlarmEngine, ModbusError] {
if max_events < 1 {
Err(CapacityExceeded)
} else {
Ok({ instances: [], events: [], max_events, next_sequence: 1 })
}
}
///|
pub fn AlarmEngine::rule_count(self : AlarmEngine) -> Int {
self.instances.length()
}
///|
pub fn AlarmEngine::event_count(self : AlarmEngine) -> Int {
self.events.length()
}
///|
pub fn AlarmEngine::add_rule(
self : AlarmEngine,
rule : AlarmRule,
) -> Result[Unit, ModbusError] {
for instance in self.instances {
if instance.id() == rule.id() {
return Err(InvalidData)
}
}
self.instances.push(AlarmInstance::new(rule))
Ok(())
}
///|
pub fn AlarmEngine::remove_rule(
self : AlarmEngine,
rule_id : String,
) -> Result[Unit, ModbusError] {
for index, instance in self.instances {
if instance.id() == rule_id {
ignore(self.instances.remove(index))
return Ok(())
}
}
Err(InvalidData)
}
///|
pub fn AlarmEngine::instance(
self : AlarmEngine,
rule_id : String,
) -> AlarmInstance? {
for instance in self.instances {
if instance.id() == rule_id {
return Some(instance)
}
}
None
}
///|
fn AlarmEngine::store_event(self : AlarmEngine, event : AlarmEvent) -> Unit {
if self.events.length() >= self.max_events {
ignore(self.events.remove(0))
}
self.events.push(event)
}
///|
pub fn AlarmEngine::evaluate(
self : AlarmEngine,
point_id : String,
value : Int,
timestamp : Int,
) -> Result[Array[AlarmEvent], ModbusError] {
let result : Array[AlarmEvent] = []
for index, instance in self.instances {
if instance.point_id() == point_id {
let transition = instance.evaluate(value, timestamp, self.next_sequence)
self.instances[index] = instance
self.next_sequence += 1
match transition {
Unchanged(_) => ()
Changed(event) => {
self.store_event(event)
result.push(event)
}
}
}
}
Ok(result)
}
///|
pub fn AlarmEngine::acknowledge(
self : AlarmEngine,
rule_id : String,
operator : String,
timestamp : Int,
) -> Result[AlarmEvent, ModbusError] {
for index, instance in self.instances {
if instance.id() == rule_id {
match instance.acknowledge(operator) {
Err(error) => return Err(error)
Ok(_) => {
self.instances[index] = instance
self.next_sequence += 1
let event = alarm_event(
instance,
Acknowledged,
instance.last_value(),
timestamp,
self.next_sequence,
)
self.store_event(event)
return Ok(event)
}
}
}
}
Err(InvalidData)
}
///|
pub fn AlarmEngine::shelve(
self : AlarmEngine,
rule_id : String,
timestamp : Int,
) -> Result[AlarmEvent, ModbusError] {
for index, instance in self.instances {
if instance.id() == rule_id {
match instance.shelve() {
Err(error) => return Err(error)
Ok(_) => {
self.instances[index] = instance
self.next_sequence += 1
let event = alarm_event(
instance,
Shelved,
instance.last_value(),
timestamp,
self.next_sequence,
)
self.store_event(event)
return Ok(event)
}
}
}
}
Err(InvalidData)
}
///|
pub fn AlarmEngine::unshelve(
self : AlarmEngine,
rule_id : String,
timestamp : Int,
) -> Result[AlarmEvent, ModbusError] {
for index, instance in self.instances {
if instance.id() == rule_id {
instance.unshelve()
self.instances[index] = instance
self.next_sequence += 1
let event = alarm_event(
instance,
Normal,
instance.last_value(),
timestamp,
self.next_sequence,
)
self.store_event(event)
return Ok(event)
}
}
Err(InvalidData)
}
///|
pub fn AlarmEngine::active(self : AlarmEngine) -> Array[AlarmInstance] {
let result : Array[AlarmInstance] = []
for instance in self.instances {
if instance.is_active() {
result.push(instance)
}
}
result
}
///|
pub fn AlarmEngine::active_count(self : AlarmEngine) -> Int {
self.active().length()
}
///|
pub fn AlarmEngine::events(self : AlarmEngine) -> Array[AlarmEvent] {
let result : Array[AlarmEvent] = []
for event in self.events {
result.push(event)
}
result
}
///|
pub fn AlarmEngine::events_for_point(
self : AlarmEngine,
point_id : String,
) -> Array[AlarmEvent] {
let result : Array[AlarmEvent] = []
for event in self.events {
if event.point_id() == point_id {
result.push(event)
}
}
result
}
///|
pub fn AlarmEngine::clear_events(self : AlarmEngine) -> Unit {
self.events.clear()
}
///|
pub fn AlarmEngine::reset(self : AlarmEngine) -> Unit {
for index, instance in self.instances {
let rule = instance.rule()
self.instances[index] = AlarmInstance::new(rule)
}
self.events.clear()
self.next_sequence = 1
}
///|
pub fn AlarmEngine::summary(self : AlarmEngine) -> String {
"rules=" +
self.rule_count().to_string() +
" active=" +
self.active_count().to_string() +
" events=" +
self.event_count().to_string()
}