Alarms and severity
✅ Verified ·
alarms.rs·demo_alarms.py· checkdocs_verify·The badge reports the whole
docs-verifysuite, not this chapter alone.
What you'll build
PVs that tell a client not just what the value is but whether it should be worried about it.
The alarm structure
Every NtScalar carries an alarm block alongside its value:
alarm:
severity: int # 0 NONE, 1 MINOR, 2 MAJOR, 3 INVALID
status: int # EPICS status code
message: string # free text
Severity is the part clients act on. A control screen turns a widget yellow on MINOR and red on MAJOR; an archiver may record the transition even when the value itself is inside the deadband.
Alarm transitions always post. The MDEL deadband gates value changes
only — a severity change reaches every subscriber regardless of how small
the value moved (should_post_update,
spvirit-server/src/simple_store.rs:644).
Two ways to get a severity
| How | Evaluated by | |
|---|---|---|
| Computed | LOW/HIGH/LOLO/HIHI in a .db file, or .alarm_limits(lolo, low, high, hihi) on a handle, plus .compute_alarms(true) | the server, on every write |
| Explicit | set_alarm(severity, status, message) | you |
Computed, from a .db file
record(ao, "DEMO:SETPOINT") {
field(LOW, "10") # MINOR below this
field(HIGH, "40") # MINOR above this
field(LOLO, "5") # MAJOR below this
field(HIHI, "45") # MAJOR above this
}
With .compute_alarms(true) the server re-evaluates on every write:
$ spput DEMO:SETPOINT 41 && spget DEMO:SETPOINT
DEMO:SETPOINT 41 MINOR READ HIGH
$ spput DEMO:SETPOINT 46 && spget DEMO:SETPOINT
DEMO:SETPOINT 46 MAJOR READ HIHI
$ spput DEMO:SETPOINT 3 && spget DEMO:SETPOINT
DEMO:SETPOINT 3 MAJOR READ LOLO
$ spput DEMO:SETPOINT 25 && spget DEMO:SETPOINT
DEMO:SETPOINT 25
compute_alarms defaults to false. Without it the limits are
published but never compared against anything.
Explicit
Rust
#![allow(unused)] fn main() { let pressure = Pv::ao("SIM:PRESSURE", 50.0) .units("bar") .desc("Vessel pressure") // lolo, low, high, hihi — published, and evaluated on every write. .alarm_limits(5.0, 10.0, 90.0, 110.0); let link = Pv::ai("SIM:LINK", 0.0).desc("Device link health"); let server = PvaServer::serve([pressure.clone(), link.clone()]) .compute_alarms(true) .build() .await; }
#![allow(unused)] fn main() { // Alarms you decide yourself, independent of the value. // severity: 0=NONE 1=MINOR 2=MAJOR 3=INVALID link.set_alarm(3, 17, "device unreachable").await?; }
Python
Same two halves. alarm_limits=(lolo, low, high, hihi) is a keyword
argument rather than a builder call:
pressure = spvirit.ai(
"SIM:PRESSURE",
50.0,
units="bar",
desc="Vessel pressure",
# lolo, low, high, hihi — published, and evaluated on every write
# because the server is started with compute_alarms=True.
alarm_limits=(5.0, 10.0, 90.0, 110.0),
)
link = spvirit.ai("SIM:LINK", 0.0, desc="Device link health")
server = spvirit.Server(pvs=[pressure, link], compute_alarms=True)
server.start()
# Alarms you decide yourself, independent of the value.
# severity: 0=NONE 1=MINOR 2=MAJOR 3=INVALID
# status is an EPICS status code; the message is free text.
link.set_alarm(3, 17, "device unreachable")
# Each write is compared with the limits above: 50 is in range, 95 raises
# MINOR (HIGH), 120 raises MAJOR (HIHI). No set_alarm needed.
for reading in (50.0, 95.0, 120.0):
pressure.set(reading)
print(f"SIM:PRESSURE = {reading}")
time.sleep(1)
What to notice
.alarm_limits() is published and evaluated. Calling
.alarm_limits(lolo, low, high, hihi) on a Pv handle — or passing
alarm_limits=(...) in Python — fills in the valueAlarm structure that
clients read back with spinfo, and also sets the limits the evaluator
reads, the same ones the .db loader sets for LOLO/LOW/HIGH/HIHI. With .compute_alarms(true) every write is
evaluated, whether it is a client spput or a host-side pv.set(...):
$ spput SIM:PRESSURE 95 && spget SIM:PRESSURE
SIM:PRESSURE 95 MINOR READ HIGH
$ spput SIM:PRESSURE 120 && spget SIM:PRESSURE
SIM:PRESSURE 120 MAJOR READ HIHI
As with a .db record, the limits are not applied to the initial value:
a PV created already past a limit reads NO_ALARM until its first write.
set_alarm remains the route for alarms that do not follow from the
value, and an explicit set_alarm is overwritten by the next computed
write while compute_alarms is on.
INVALID is for "I don't know", not "bad value". Severity 3 means the reading cannot be trusted — the device is unreachable, the scan threw, the link is down. A value that is simply too high is MAJOR, not INVALID. This matters because clients treat INVALID as "ignore this number".
A failing calc posts 0.0 with severity NONE. It looks like a
genuine reading of zero. If a computed PV must distinguish broken from
zero, set INVALID explicitly — see Simulating a device.
set_alarm bypasses the deadband and does not evaluate links. It is a
direct write to the alarm block.
Run it
# Terminal 1
cargo run -p spvirit-server --example alarms
# or: python spvirit-py/examples/demo_alarms.py
# Terminal 2
spget SIM:LINK # INVALID device unreachable
spmonitor SIM:PRESSURE
$ spget SIM:LINK
SIM:LINK 2026-08-06 09:14:45.114 0 INVALID SIMM device unreachable
$ spget SIM:PRESSURE
SIM:PRESSURE 2026-08-06 09:14:45.114 50
spget appends severity, status and message after the value when the
alarm is not NO_ALARM — and prints nothing extra when it is, which is why
SIM:PRESSURE at 50 looks like a plain reading.
The Rust alarms example is static, so spmonitor SIM:PRESSURE shows one
line and then waits. Use the Python example to watch the limits fire:
python spvirit-py/examples/demo_alarms.py # terminal 1
spmonitor SIM:PRESSURE # terminal 2
SIM:PRESSURE 2026-08-06 09:26:17.437 50
SIM:PRESSURE 2026-08-06 09:26:18.438 95
SIM:PRESSURE {alarm={severity=1, status=1, message="HIGH"}} MINOR READ HIGH
SIM:PRESSURE 2026-08-06 09:26:19.439 120
SIM:PRESSURE {alarm={severity=2, status=1, message="HIHI"}} MAJOR READ HIHI
Two things worth pausing on. The value and the alarm arrive as separate
updates — the server changed value first, then the record layer
evaluated the limits and changed alarm. And the second line of each pair
shows only the field that changed, because a monitor sends a change bitset;
spmonitor renders the changed subtree and then the decoded severity.