Alarm Helpers¶
The AnalogAlarm and DigitalAlarm classes provide convenient interfaces for reading and modifying ACNET alarm blocks. On the backend, they read/write .RAW byte arrays and fields as necessary - there is no magic.
Reference
For low-level alarm block structure details, see the MOOC Property Documentation.
Overview¶
| Type | Monitors | Alarms When |
|---|---|---|
| Analog | Numeric values | Value outside min/max range |
| Digital | Bit patterns | (reading & mask) != (nominal & mask) |
from pacsys.alarm_block import AnalogAlarm, DigitalAlarm
# Quick read
analog = AnalogAlarm.read("Z:ACLTST")
digital = DigitalAlarm.read("Z:ACLTST")
print(f"Analog: {analog.minimum} to {analog.maximum}")
print(f"Digital: nominal=0x{digital.nominal:X}, mask=0x{digital.mask:X}")
Context Manager (Recommended)¶
The modify() context manager handles read-modify-write automatically:
from pacsys.alarm_block import AnalogAlarm, DigitalAlarm
# Analog: set temperature limits (engineering units)
with AnalogAlarm.modify("Z:ACLTST") as alarm:
alarm.minimum = 32.0 # Engineering units (e.g., Fahrenheit)
alarm.maximum = 100.0
alarm.bypass = False
# Digital: require bit 0 set
with DigitalAlarm.modify("Z:ACLTST") as alarm:
alarm.nominal = 0x0001
alarm.mask = 0x0001
alarm.bypass = False
Alarm state is read on context entrance and changes are written on context exit; nothing is written if no changes were made or an exception occurs.
Digital nominal/mask and shared tries_needed changes are taken from the current
alarm object. Raw-only changes such as FTD are written through the raw alarm block. If
both raw and structured fields change together, modify() preserves both while
retaining the server-transformed analog limits.
Engineering Units¶
Both read() and modify() fetch engineering ('common') values. If you read alarm channels as .RAW directly, you will get the raw byte values without raw-to-primary-to-common transforms.
Since we don't know how to convert raw to common on the client, writing can get complicated. The context manager automatically determines optimal write strategy based on what fields you changed.
With Explicit Backend¶
from pacsys import KerberosAuth
import pacsys
auth = KerberosAuth()
with pacsys.dpm(auth=auth, role="testing") as backend:
with AnalogAlarm.modify("Z:ACLTST", backend=backend) as alarm:
alarm.bypass = True
Manual Control¶
As usual, you can use read() and write() separately:
from pacsys.alarm_block import AnalogAlarm
# Read
alarm = AnalogAlarm.read("Z:ACLTST")
print(f"Current: {alarm}")
# Inspect and modify
alarm.bypass = True
alarm.write("Z:ACLTST")
# do naughty stuff
# Set back
alarm.bypass = False
alarm.write("Z:ACLTST")
Note: for manual-style writes, all alarm fields are written every time.
Common Properties¶
Both alarm types share these properties:
| Property | Type | Description |
|---|---|---|
is_active |
bool |
True if alarm is active (not bypassed) |
bypass |
bool |
True if bypassed (inverse of is_active) |
is_bad |
bool |
True if currently in alarm state |
abort |
bool |
Read-only abort flag (AB bit) |
abort_inhibit |
bool |
Abort inhibit flag (AI bit) |
abort_enabled |
bool |
True if alarm can trigger abort |
tries_needed |
int |
Consecutive bad readings before alarm |
tries_now |
int |
Current consecutive bad count |
ftd |
FTD |
Sampling configuration |
data_length |
DataLength |
Value byte size (1, 2, or 4) |
Analog Alarm¶
Monitors numeric values against configurable limits.
Limit Types¶
Use minimum and maximum to set limits in engineering units:
from pacsys.alarm_block import AnalogAlarm
with AnalogAlarm.modify("Z:ACLTST") as alarm:
alarm.minimum = 0.0 # Engineering units
alarm.maximum = 100.0
NOM_TOL vs MIN_MAX¶
Just use minimum/maximum in engineering units -- DPM handles the NOM_TOL/MIN_MAX conversion server-side. To set a nom/tol-style alarm, convert to min/max yourself: minimum = nom - tol, maximum = nom + tol.
Raw alarm block internals
The raw 20-byte alarm block has a limit_type flag (K bits 8-9) that controls how
the two 4-byte value fields are interpreted:
limit_type |
K bits | value1 meaning | value2 meaning |
|---|---|---|---|
MIN_MAX |
0b10 |
minimum | maximum |
NOM_TOL |
0b00 |
nominal | tolerance |
These raw values are in primary (raw) units - the integer or float stored in the
front-end hardware before any scaling transform is applied. The minimum/maximum
properties, on the other hand, are in engineering (common) units - the scaled values
returned by DPM after applying the device's raw-to-common transform.
How DPM handles this server-side¶
When you write a structured alarm field (e.g., minimum, maximum), DPM performs a
read-modify-write on the 20-byte alarm block for every field write. It reads the
current block, checks the K bits, modifies the raw values accordingly, and writes the
block back. The K bits are never changed by a field write - DPM adapts the
coordinate system instead:
- Writing
minimum/maximumto a MIN_MAX device: values are stored directly in value1/value2 after unscaling from engineering to raw units. - Writing
minimum/maximumto a NOM_TOL device: DPM converts to nominal/tolerance coordinates:nominal = (min + max) / 2,tolerance = (max - min) / 2, then stores in value1/value2. - Writing
nominal/toleranceto a NOM_TOL device: stored directly. - Writing
nominal/toleranceto a MIN_MAX device: DPM converts to min/max coordinates:min = nom - tol,max = nom + tol.
This means minimum/maximum always work correctly regardless of the device's
underlying limit mode - DPM transparently handles the conversion.
No backend protocol (DPM PC binary, gRPC protobuf, DMQ SDD) has structured fields for
nominal/tolerance - they all only expose minimum and maximum in engineering units.
The limit_type flag is only accessible via raw byte writes, but writing raw values
requires knowing the device's transform, which pacsys cannot do client-side.
nominal = 50.0
tolerance = 5.0
with AnalogAlarm.modify("Z:ACLTST") as alarm:
alarm.minimum = nominal - tolerance # 45.0 (engineering units)
alarm.maximum = nominal + tolerance # 55.0
# DPM stores as nom=50, tol=5 if device is in NOM_TOL mode
You can still read the limit_type flag to check which mode a device uses, and
access the raw values via value1/value2:
from pacsys.alarm_block import AnalogAlarm, LimitType
alarm = AnalogAlarm.read("Z:ACLTST")
if alarm.limit_type == LimitType.NOM_TOL:
print(f"Nominal (raw units): {alarm.value1}")
print(f"Tolerance (raw units): {alarm.value2}")
else:
print(f"Min (eng units): {alarm.minimum}")
print(f"Max (eng units): {alarm.maximum}")
Changing value1 or value2 inside modify() performs a raw alarm-block
write. Do not change the corresponding engineering-unit property in the same
operation.
Changing minimum or maximum together with limit_type, data_type, or
data_length raises ValueError before any write. Those fields change how raw
limits are interpreted, so they cannot be combined with server-scaled limit edits.
Analog-Specific Properties¶
| Property | Type | Description |
|---|---|---|
limit_type |
LimitType |
MIN_MAX or NOM_TOL (read-only useful; see above) |
minimum |
float or None |
Minimum in engineering units |
maximum |
float or None |
Maximum in engineering units |
value1 |
int or float |
Raw value 1 (min or nominal, primary units) |
value2 |
int or float |
Raw value 2 (max or tolerance, primary units) |
is_high |
bool |
Reading exceeds high limit |
is_low |
bool |
Reading below low limit |
data_type |
DataType |
Value type (signed/unsigned/float) |
Digital Alarm¶
Monitors bit patterns using mask comparison: (reading & mask) != (nominal & mask)
Mask Examples¶
from pacsys.alarm_block import DigitalAlarm
with DigitalAlarm.modify("Z:ACLTST") as alarm:
# Alarm if bit 0 not set
alarm.nominal = 0x0001
alarm.mask = 0x0001
# Alarm if bits 0-3 don't equal 0x05
alarm.nominal = 0x0005
alarm.mask = 0x000F
# Only check bit 7, ignore others
alarm.nominal = 0x0080
alarm.mask = 0x0080
Digital-Specific Properties¶
| Property | Type | Description |
|---|---|---|
nominal |
int |
Expected bit pattern |
mask |
int |
Which bits to check |
FTD (Sampling Configuration)¶
Controls how often the alarm system samples the device. Equivalent to DRF3 @p and @e events.
from pacsys.alarm_block import FTD
# Periodic sampling
alarm.ftd = FTD.periodic_hz(1.0) # 1 Hz
alarm.ftd = FTD.periodic_ticks(60) # 60Hz ticks (= 1 Hz)
# Event-triggered
alarm.ftd = FTD.on_event(0x0F) # On TCLK event $0F
alarm.ftd = FTD.on_event(0x0F, delay_ms=100) # With 100ms delay
# Device default (what D80 shows)
alarm.ftd = FTD.default()
Alarm Segments¶
Structured alarm reads and modifications support segment 0 only because the server ignores ranges for structured alarm fields. Access additional segments through range-qualified raw blocks:
raw = backend.read("Z:ACLTST.ANALOG{20:20}.RAW@I")
alarm1 = AnalogAlarm.from_bytes(raw)
# Raw writes preserve the segment offset.
alarm1.write("Z:ACLTST", backend=backend, segment=1)
Error Handling¶
from pacsys.alarm_block import AnalogAlarm
from pacsys.errors import DeviceError
try:
alarm = AnalogAlarm.read("INVALID:DEVICE")
except DeviceError as e:
print(f"Cannot read alarm: {e}")
# Common: DBM_NOPROP if device has no analog alarm
Complete Example¶
from pacsys.alarm_block import AnalogAlarm, DigitalAlarm, FTD
from pacsys import KerberosAuth
import pacsys
auth = KerberosAuth()
with pacsys.dpm(auth=auth, role="testing") as backend:
# Configure analog alarm for temperature (engineering units)
with AnalogAlarm.modify("Z:TEMP", backend=backend) as alarm:
alarm.minimum = 32.0 # Engineering units (DPM handles raw conversion)
alarm.maximum = 100.0
alarm.ftd = FTD.periodic_hz(0.5)
alarm.tries_needed = 3
alarm.bypass = False
# Configure digital alarm for interlock
with DigitalAlarm.modify("Z:INTLK", backend=backend) as alarm:
alarm.nominal = 0x0007 # Bits 0,1,2 must be set
alarm.mask = 0x0007
alarm.ftd = FTD.periodic_hz(60.0)
alarm.tries_needed = 1
alarm.bypass = False
Dict Write Shortcut¶
Use DPM/HTTP (pacsys.dpm()) to write alarm fields as a dict:
backend.write("Z@ACLTST", {"minimum": 40.0, "maximum": 50.0, "alarm_enable": True})
backend.write("Z$ACLTST", {"nominal": 0x0001, "mask": 0x00FF})
DPM/HTTP expands the dict into sequential per-field writes (e.g.,
DEVICE.ANALOG.MIN, DEVICE.ANALOG.MAX). Each field triggers a server-side
read-modify-write of the 20-byte alarm block. The operation is not atomic: if a
later field fails, earlier changes remain applied.
DPM/gRPC alarm dict writes are unsupported: the client returns a failed
WriteResult without sending the alarm dict. DMQ support on deployed servers is unverified;
the reference server rejects structured alarm writes. Use DPM/HTTP for this shortcut.
Read-only keys (abort, alarm_status, tries_now) raise ValueError.
See Writing Guide - Alarm Configuration for details.
See Also¶
- Writing Guide - General write operations and alarm dict writes
- DRF Format - Device request format reference
- MOOC Property Documentation - Low-level alarm block details