Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Unreleased_

Fixed:

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

4.7.0_ - 2026-01-14
Expand Down
7 changes: 7 additions & 0 deletions softioc/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ def set(self, value, process=True,
# However if we do not process, we must do this here to keep the
# Python and EPICS values in line
if not process:
# Normally a call to dbProcess would handle setting/re-setting
# alarm levels for us, however if we are not processing we
# obviously can't rely on that. Hence we have to do it
# ourselves. We don't use `process_severity` as we want to
# unconditionally set these, regardless of previous severity.
_record.NSEV = severity
_record.NSTA = alarm
self._value = (value, severity, alarm)

db_put_field_process(_record.NAME, dbf_code, data, length, process)
Expand Down
3 changes: 3 additions & 0 deletions softioc/device_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ def _get_ioinit_info(self, cmd, record, ppvt):

def process_severity(self, record, severity, alarm):
'''Support routine to implement alarm processing.'''
# This matches the logic in the EPICS function recGblSetSevr, which
# always maximises severity. This is called as part of EPICS record
# processing
if severity > record.NSEV:
record.NSEV = severity
record.NSTA = alarm
Expand Down
131 changes: 126 additions & 5 deletions tests/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,8 +683,8 @@ def test_on_update_true_false(self, out_records):
always_update is True and the put'ed value is always different"""
self.on_update_runner(out_records, True, False)

def on_update_recursive_set_test_func(
self, device_name, conn
def on_update_recursive_set_value_test_func(
self, device_name, conn,
):
log("CHILD: Child started")

Expand Down Expand Up @@ -718,8 +718,8 @@ async def on_update_func(new_val):

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

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

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

process = ctx.Process(
target=self.on_update_recursive_set_test_func,
target=self.on_update_recursive_set_value_test_func,
args=(device_name, child_conn),
)

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


def on_update_recursive_set_alarm_test_func(
self, device_name, conn, severity, alrm
):
log("CHILD: Child started")

builder.SetDeviceName(device_name)

async def on_update_func(new_val):
log(f"CHILD: on_update_func started value {new_val}")
if new_val == 2:
record.set(
new_val,
process=False,
severity=severity,
alarm=alrm
)
elif new_val == 3:
record.set(
new_val,
process=False,
severity=alarm.NO_ALARM,
alarm=alarm.NO_ALARM
)
conn.send("C") # "Callback"
log("CHILD: on_update_func ended")

record = builder.longOut(
"ACTION",
on_update=on_update_func,
blocking=True,
initial_value=1 # A non-zero value, to check it changes
)

dispatcher = asyncio_dispatcher.AsyncioDispatcher()
builder.LoadDatabase()
softioc.iocInit(dispatcher)

conn.send("R") # "Ready"

log("CHILD: Sent R over Connection to Parent")

# Keep process alive while main thread runs CAGET
if conn.poll(TIMEOUT):
val = conn.recv()
assert val == "D", "Did not receive expected Done character"

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

async def test_on_update_recursive_set_alarm(self):
"""Test that on_update sets alarms correctly when the on_update
callback sets the value of the record again (with process=False).
See PR #209"""

ctx = get_multiprocessing_context()
parent_conn, child_conn = ctx.Pipe()

device_name = create_random_prefix()

expected_sevr = alarm.MAJOR_ALARM
expected_alarm = alarm.HIGH_ALARM # arbitrary choice

process = ctx.Process(
target=self.on_update_recursive_set_alarm_test_func,
args=(device_name, child_conn, expected_sevr, expected_alarm),
)

process.start()

log("PARENT: Child started, waiting for R command")

from aioca import caget, caput

try:
# Wait for message that IOC has started
select_and_recv(parent_conn, "R")

log("PARENT: received R command")

record = f"{device_name}:ACTION"

val = await caget(record)

assert val == 1, "ACTION record did not start with value 1"

# Trigger callback to set alarm states
await caput(record, 2, wait=True)

sevr = await caget(record + ".SEVR")
assert sevr == expected_sevr

sevr = await caget(record + ".STAT")
assert sevr == expected_alarm

# Expect one "C"
select_and_recv(parent_conn, "C")

# Trigger another callback to clear alarm states
await caput(record, 3, wait=True)

sevr = await caget(record + ".SEVR")
assert sevr == alarm.NO_ALARM

sevr = await caget(record + ".STAT")
assert sevr == alarm.NO_ALARM

# Expect second "C"
select_and_recv(parent_conn, "C")

# ...But if we receive another we know there's a problem
if parent_conn.poll(5): # Shorter timeout to make this quicker
pytest.fail("Received unexpected second message")

finally:
log("PARENT:Sending Done command to child")
parent_conn.send("D") # "Done"
process.join(timeout=TIMEOUT)
log(f"PARENT: Join completed with exitcode {process.exitcode}")
if process.exitcode is None:
pytest.fail("Process did not terminate")



class TestBlocking:
"""Tests related to the Blocking functionality"""
Expand Down
Loading