Skip to content

Commit e59476a

Browse files
Merge pull request #209 from DiamondLightSource/blocking_alarm_fixes
Correctly set alarms during blocking processing
2 parents 1711080 + 6b991a4 commit e59476a

4 files changed

Lines changed: 137 additions & 5 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Unreleased_
1212

1313
Fixed:
1414

15+
- `Correctly set alarms during blocking processing <../../pull/209>`_
1516
- `Fix infinite loop when setting record's own value from on_update callback <../../pull/202>`_
1617

1718
4.7.0_ - 2026-01-14

softioc/device.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,13 @@ def set(self, value, process=True,
291291
# However if we do not process, we must do this here to keep the
292292
# Python and EPICS values in line
293293
if not process:
294+
# Normally a call to dbProcess would handle setting/re-setting
295+
# alarm levels for us, however if we are not processing we
296+
# obviously can't rely on that. Hence we have to do it
297+
# ourselves. We don't use `process_severity` as we want to
298+
# unconditionally set these, regardless of previous severity.
299+
_record.NSEV = severity
300+
_record.NSTA = alarm
294301
self._value = (value, severity, alarm)
295302

296303
db_put_field_process(_record.NAME, dbf_code, data, length, process)

softioc/device_core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def _get_ioinit_info(self, cmd, record, ppvt):
177177

178178
def process_severity(self, record, severity, alarm):
179179
'''Support routine to implement alarm processing.'''
180+
# This matches the logic in the EPICS function recGblSetSevr, which
181+
# always maximises severity. This is called as part of EPICS record
182+
# processing
180183
if severity > record.NSEV:
181184
record.NSEV = severity
182185
record.NSTA = alarm

tests/test_records.py

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -683,8 +683,8 @@ def test_on_update_true_false(self, out_records):
683683
always_update is True and the put'ed value is always different"""
684684
self.on_update_runner(out_records, True, False)
685685

686-
def on_update_recursive_set_test_func(
687-
self, device_name, conn
686+
def on_update_recursive_set_value_test_func(
687+
self, device_name, conn,
688688
):
689689
log("CHILD: Child started")
690690

@@ -718,8 +718,8 @@ async def on_update_func(new_val):
718718

719719
log("CHILD: Received exit command, child exiting")
720720

721-
async def test_on_update_recursive_set(self):
722-
"""Test that on_update functions correctly when the on_update
721+
async def test_on_update_recursive_set_value(self):
722+
"""Test that on_update sets values correctly when the on_update
723723
callback sets the value of the record again (with process=False).
724724
See issue #201"""
725725

@@ -729,7 +729,7 @@ async def test_on_update_recursive_set(self):
729729
device_name = create_random_prefix()
730730

731731
process = ctx.Process(
732-
target=self.on_update_recursive_set_test_func,
732+
target=self.on_update_recursive_set_value_test_func,
733733
args=(device_name, child_conn),
734734
)
735735

@@ -773,6 +773,127 @@ async def test_on_update_recursive_set(self):
773773
pytest.fail("Process did not terminate")
774774

775775

776+
def on_update_recursive_set_alarm_test_func(
777+
self, device_name, conn, severity, alrm
778+
):
779+
log("CHILD: Child started")
780+
781+
builder.SetDeviceName(device_name)
782+
783+
async def on_update_func(new_val):
784+
log(f"CHILD: on_update_func started value {new_val}")
785+
if new_val == 2:
786+
record.set(
787+
new_val,
788+
process=False,
789+
severity=severity,
790+
alarm=alrm
791+
)
792+
elif new_val == 3:
793+
record.set(
794+
new_val,
795+
process=False,
796+
severity=alarm.NO_ALARM,
797+
alarm=alarm.NO_ALARM
798+
)
799+
conn.send("C") # "Callback"
800+
log("CHILD: on_update_func ended")
801+
802+
record = builder.longOut(
803+
"ACTION",
804+
on_update=on_update_func,
805+
blocking=True,
806+
initial_value=1 # A non-zero value, to check it changes
807+
)
808+
809+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
810+
builder.LoadDatabase()
811+
softioc.iocInit(dispatcher)
812+
813+
conn.send("R") # "Ready"
814+
815+
log("CHILD: Sent R over Connection to Parent")
816+
817+
# Keep process alive while main thread runs CAGET
818+
if conn.poll(TIMEOUT):
819+
val = conn.recv()
820+
assert val == "D", "Did not receive expected Done character"
821+
822+
log("CHILD: Received exit command, child exiting")
823+
824+
async def test_on_update_recursive_set_alarm(self):
825+
"""Test that on_update sets alarms correctly when the on_update
826+
callback sets the value of the record again (with process=False).
827+
See PR #209"""
828+
829+
ctx = get_multiprocessing_context()
830+
parent_conn, child_conn = ctx.Pipe()
831+
832+
device_name = create_random_prefix()
833+
834+
expected_sevr = alarm.MAJOR_ALARM
835+
expected_alarm = alarm.HIGH_ALARM # arbitrary choice
836+
837+
process = ctx.Process(
838+
target=self.on_update_recursive_set_alarm_test_func,
839+
args=(device_name, child_conn, expected_sevr, expected_alarm),
840+
)
841+
842+
process.start()
843+
844+
log("PARENT: Child started, waiting for R command")
845+
846+
from aioca import caget, caput
847+
848+
try:
849+
# Wait for message that IOC has started
850+
select_and_recv(parent_conn, "R")
851+
852+
log("PARENT: received R command")
853+
854+
record = f"{device_name}:ACTION"
855+
856+
val = await caget(record)
857+
858+
assert val == 1, "ACTION record did not start with value 1"
859+
860+
# Trigger callback to set alarm states
861+
await caput(record, 2, wait=True)
862+
863+
sevr = await caget(record + ".SEVR")
864+
assert sevr == expected_sevr
865+
866+
sevr = await caget(record + ".STAT")
867+
assert sevr == expected_alarm
868+
869+
# Expect one "C"
870+
select_and_recv(parent_conn, "C")
871+
872+
# Trigger another callback to clear alarm states
873+
await caput(record, 3, wait=True)
874+
875+
sevr = await caget(record + ".SEVR")
876+
assert sevr == alarm.NO_ALARM
877+
878+
sevr = await caget(record + ".STAT")
879+
assert sevr == alarm.NO_ALARM
880+
881+
# Expect second "C"
882+
select_and_recv(parent_conn, "C")
883+
884+
# ...But if we receive another we know there's a problem
885+
if parent_conn.poll(5): # Shorter timeout to make this quicker
886+
pytest.fail("Received unexpected second message")
887+
888+
finally:
889+
log("PARENT:Sending Done command to child")
890+
parent_conn.send("D") # "Done"
891+
process.join(timeout=TIMEOUT)
892+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
893+
if process.exitcode is None:
894+
pytest.fail("Process did not terminate")
895+
896+
776897

777898
class TestBlocking:
778899
"""Tests related to the Blocking functionality"""

0 commit comments

Comments
 (0)