ReferenceControlArcHow ToAlarms

Alarms

Trigger warnings and notifications when sensor values exceed limits

Alarms notify operators when values exceed acceptable limits. A pressure that crosses 600 psi should trigger a warning. A temperature that stays above 300°C for more than 5 seconds should sound an alarm. Arc makes these patterns straightforward.

Basic Threshold

The simplest alarm compares a value to a threshold.

tank_level < 10.0 -> status.set{ ... }
tank_pressure > 9000.0 -> status.set{ ... }

Time-Delayed

A brief spike might be acceptable, but a sustained high value indicates a real problem. Debounce the condition with stable.for so the alarm fires only after it holds:

tank_pressure > 600 -> stable.for{duration=1s} => alert_hq

Deadband

Simple threshold alarms chatter when values oscillate near the limit. If pressure hovers around 600 psi, the alarm toggles on and off rapidly. A deadband prevents this by requiring the value to cross back below a lower threshold before the alarm clears:

func deadband_alarm{
    high f64, // alarm activates above this
    low f64,  // alarm clears below this
} (value f64) bool {
    alarm_active $= false

    if not alarm_active {
        // Alarm is off, check to activate
        if value > high {
            alarm_active = true
        }
    } else {
        // Alarm is on, check to clear
        if value < low {
            alarm_active = false
        }
    }

    return alarm_active
}

tank_pressure -> deadband_alarm{high=600.0, low=580.0} -> pressure_high

The alarm activates when pressure exceeds 600 psi. It only clears when pressure drops below 580 psi. This 20 psi deadband eliminates chatter.

Choose your deadband based on expected noise and process dynamics. Too narrow and you still get chatter. Too wide and the alarm takes too long to clear.

High and Low with Deadband

Many sensors need both high and low alarms. Create separate functions for each:

func low_deadband_alarm{
    low f64,   // alarm activates below this
    clear f64, // alarm clears above this
} (value f64) bool {
    state $= false

    if not state {
        if value < low {
            state = true
        }
    } else {
        if value > clear {
            state = false
        }
    }

    return state
}
// Use separate flows for high and low alarms
tank_pressure -> deadband_alarm{high=600.0, low=580.0} -> pressure_high
tank_pressure -> low_deadband_alarm{low=100.0, clear=120.0} -> pressure_low

Multi-Condition

Some alarms require multiple conditions. An abort alarm might trigger when both inlet and outlet pressures are elevated (potential runaway), or when any single pressure exceeds a critical limit:

func combined_alarm{
    inlet chan f64,
    outlet chan f64,
    inlet_crit f64,
    outlet_crit f64,
    combined_inlet f64,
    combined_outlet f64,
} () bool {
    p1 := inlet
    p2 := outlet

    // Critical: either exceeds maximum
    critical := p1 > inlet_crit or p2 > outlet_crit
    // Combined: both elevated (potential runaway)
    combined := p1 > combined_inlet and p2 > combined_outlet

    return critical or combined
}

time.interval{period=50ms} -> combined_alarm{
    inlet = inlet_pressure,
    outlet = outlet_pressure,
    inlet_crit = 800.0,
    outlet_crit = 600.0,
    combined_inlet = 500.0,
    combined_outlet = 400.0
} -> abort_trigger

Latching

Some alarms should stay active until manually acknowledged, even if the condition clears:

func latching_alarm{
    limit f64,
    ack chan bool, // acknowledgment channel (write true to clear)
} (value f64) bool {
    latched $= false

    // Check for acknowledgment
    ack_signal := ack
    if ack_signal {
        latched = false
    }

    // Activate on threshold
    if value > limit {
        latched = true
    }

    return latched
}

tank_pressure -> latching_alarm{limit=600.0, ack=alarm_ack} -> latched_alarm

Wire alarm_ack to a button in your Console schematic. The alarm stays active until the operator acknowledges it by clicking the button.

Priority Levels

The simplest form maps a reading to a priority level and logs it:

func classify{warn f64, critical f64} (value f64) str {
    if value >= critical {
        return "Critical"
    }
    if value >= warn {
        return "Warning"
    }
    return ""
}

// "" is falsy, so => never fires for nominal readings
tank_pressure -> classify{warn=600.0, critical=700.0} => log_channel

When each level warrants a different response, demux the value with a custom routing table. A warning sets the status, and a critical also triggers the abort stage:

import status

func prioritize{
    warn f64,
    crit f64,
} (value f64) (nominal bool, warning bool, critical bool) {
    if value < warn {
        nominal = true
    } else if value < crit {
        warning = true
    } else {
        critical = true
    }
}

stage monitor {
    tank_pressure -> prioritize{warn=600.0, crit=700.0} -> {
        // nominal: no-op
        warning:  "Warning" -> status.set{ ... },
        critical: sequence {
            "Critical" -> status.set{ ... },
            true => abort
        }
    }
}

Alarms in Sequences

Alarms are often used to trigger sequence transitions:

sequence main {
    stage pressurize {
        // Safety abort conditions (listed first for priority)
        tank_pressure > 700 => abort
        outlet_pressure > 500 => abort
        abort_btn => abort

        // Normal completion
        true -> valve_cmd
        tank_pressure > 500 => next
    }

    stage hold {
        // Maintain pressure, watch for problems
        tank_pressure -> deadband_alarm{high=600.0, low=580.0} -> pressure_high
        pressure_high => abort
        time.wait{duration=30s} => next
    }

    stage complete {
        false -> valve_cmd
    }
}

sequence abort {
    stage safed {
        false -> valve_cmd
        false -> vent_valve
    }
}

start_cmd => main

Always list safety-critical abort conditions first in a stage. When multiple conditional transitions (=>) are truthy in the same cycle, the first one wins.