From 50d8a43144bf3c12c542edfe4efca7072a208d26 Mon Sep 17 00:00:00 2001 From: Xavier GOIZIOU Date: Wed, 3 Apr 2024 17:17:45 +0200 Subject: [PATCH 01/10] Add the non_interactive_ioc() function modified: softioc/asyncio_dispatcher.py modified: softioc/cothread_dispatcher.py modified: softioc/softioc.py --- softioc/asyncio_dispatcher.py | 18 ++++++++++++++++++ softioc/cothread_dispatcher.py | 5 +++++ softioc/softioc.py | 14 ++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index eb186771..126d7440 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -3,6 +3,7 @@ import logging import threading import atexit +import signal class AsyncioDispatcher: def __init__(self, loop=None, debug=False): @@ -47,6 +48,23 @@ def close(self): self.__atexit = None self.__shutdown() + + def wait_for_quit(self): + + stop_event = threading.Event() + + # Signal end of loop + async def stop_loop(): + stop_event.set() + + def signal_exit(): + asyncio.run_coroutine_threadsafe(stop_loop(), self.loop) + + # Configure signal handlers to call signal_exit + for sig in ('SIGINT', 'SIGTERM'): + self.loop.add_signal_handler(getattr(signal, sig), signal_exit) + + stop_event.wait() async def __inloop(self, started): self.loop = asyncio.get_running_loop() diff --git a/softioc/cothread_dispatcher.py b/softioc/cothread_dispatcher.py index 87e1a125..d8404b09 100644 --- a/softioc/cothread_dispatcher.py +++ b/softioc/cothread_dispatcher.py @@ -19,6 +19,11 @@ def __init__(self, dispatcher = None): else: self.__dispatcher = dispatcher + def wait_for_quit(): + cothread.WaitForQuit() + + self.wait_for_quit = wait_for_quit + def __call__( self, func, diff --git a/softioc/softioc.py b/softioc/softioc.py index 884b72da..f0aaaacb 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -352,3 +352,17 @@ def interactive_ioc(context = {}, call_exit = True): if call_exit: safeEpicsExit(0) + +def non_interactive_ioc(): + '''Launches IOC in non-interactive mode for background use + + When used with a service manager, use python's -u option or the environment + variable PYTHONUNBUFFERED=TRUE. + This ensures that python output, i.e. stdoute and stderr streams, is sent + directly to the terminal. + ''' + if device.dispatcher: + device.dispatcher.wait_for_quit() + safeEpicsExit(0) + else: + print("No dispatcher found") From b4c24dd3f0b5b9ec678eeff2b1e03d93fe188158 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 4 Apr 2024 08:35:42 +0100 Subject: [PATCH 02/10] Explicitly note that Python3.6 support is removed --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 199a4011..6dda3b03 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,10 @@ Versioning `_. Unreleased_ ----------- +Removed: + +- `Remove python3.6 support <../../pull/138>`_ + Changed: - `AsyncioDispatcher cleanup tasks atexit <../../pull/138>`_ From 3a5d806577e2be2827f78e51a36029216ef4d409 Mon Sep 17 00:00:00 2001 From: Xavier GOIZIOU Date: Fri, 5 Apr 2024 10:21:32 +0200 Subject: [PATCH 03/10] Modification of the wait_for_quit function to correct a bug --- softioc/asyncio_dispatcher.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index 126d7440..3d5caae0 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -51,18 +51,20 @@ def close(self): def wait_for_quit(self): + async_stop_event = asyncio.Event() stop_event = threading.Event() - # Signal end of loop - async def stop_loop(): - stop_event.set() - def signal_exit(): - asyncio.run_coroutine_threadsafe(stop_loop(), self.loop) + self.loop.call_soon_threadsafe(async_stop_event.set) + + async def async_signal_exit(): + await async_stop_event.wait() + stop_event.set() - # Configure signal handlers to call signal_exit for sig in ('SIGINT', 'SIGTERM'): - self.loop.add_signal_handler(getattr(signal, sig), signal_exit) + signal.signal(getattr(signal, sig), lambda signum, frame: signal_exit()) + + asyncio.run_coroutine_threadsafe(async_signal_exit(), self.loop) stop_event.wait() From 54f9567c5dd72bfe6c4ce3100844ff73d46a19e3 Mon Sep 17 00:00:00 2001 From: Xavier G <104097021+XavSPM@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:50:39 +0200 Subject: [PATCH 04/10] Update softioc/softioc.py Update cothread_dispatcher.py Update asyncio_dispatcher.py Co-authored-by: Alexander Wells <79699091+AlexanderWells-diamond@users.noreply.github.com> --- softioc/asyncio_dispatcher.py | 7 +++---- softioc/cothread_dispatcher.py | 5 +---- softioc/softioc.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index 3d5caae0..5506d3fe 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -50,7 +50,6 @@ def close(self): self.__shutdown() def wait_for_quit(self): - async_stop_event = asyncio.Event() stop_event = threading.Event() @@ -61,9 +60,9 @@ async def async_signal_exit(): await async_stop_event.wait() stop_event.set() - for sig in ('SIGINT', 'SIGTERM'): - signal.signal(getattr(signal, sig), lambda signum, frame: signal_exit()) - + for sig in (signal.SIGINT, signal.SIGTERM): + signal.signal(sig, lambda signum, frame: signal_exit()) + asyncio.run_coroutine_threadsafe(async_signal_exit(), self.loop) stop_event.wait() diff --git a/softioc/cothread_dispatcher.py b/softioc/cothread_dispatcher.py index d8404b09..3f9d23b8 100644 --- a/softioc/cothread_dispatcher.py +++ b/softioc/cothread_dispatcher.py @@ -19,10 +19,7 @@ def __init__(self, dispatcher = None): else: self.__dispatcher = dispatcher - def wait_for_quit(): - cothread.WaitForQuit() - - self.wait_for_quit = wait_for_quit + self.wait_for_quit = cothread.WaitForQuit def __call__( self, diff --git a/softioc/softioc.py b/softioc/softioc.py index f0aaaacb..c06e1c2f 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -358,7 +358,7 @@ def non_interactive_ioc(): When used with a service manager, use python's -u option or the environment variable PYTHONUNBUFFERED=TRUE. - This ensures that python output, i.e. stdoute and stderr streams, is sent + This ensures that python output, i.e. stdout and stderr streams, is sent directly to the terminal. ''' if device.dispatcher: From bccc75a64191c42b62c673897e13107068496ad5 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 8 Apr 2024 11:19:20 +0100 Subject: [PATCH 05/10] Enable setting alarm status of Out records --- CHANGELOG.rst | 3 ++ docs/reference/api.rst | 17 ++++-- softioc/device.py | 66 ++++++++++++++++------- tests/test_records.py | 118 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 176 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6dda3b03..be06782e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ Versioning `_. Unreleased_ ----------- +Added: +- `Enable setting alarm status of Out records <../../pull/157>`_ + Removed: - `Remove python3.6 support <../../pull/138>`_ diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 2fb01518..ca916d4b 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -454,9 +454,10 @@ starting the IOC. Alarm Value Definitions: `softioc.alarm` ---------------------------------------- - The following values can be passed to IN record :meth:`~softioc.device.ProcessDeviceSupportIn.set` and -:meth:`~softioc.device.ProcessDeviceSupportIn.set_alarm` methods. +:meth:`~softioc.device.ProcessDeviceSupportIn.set_alarm` methods, and to OUT record +:meth:`~softioc.device.ProcessDeviceSupportOut.set` and +:meth:`~softioc.device.ProcessDeviceSupportOut.set_alarm`. .. attribute:: NO_ALARM = 0 @@ -608,14 +609,22 @@ Working with OUT records ``ao``, ``bo``, ``longout``, ``mbbo`` and OUT ``waveform`` records. All OUT records support the following methods. - .. method:: set(value, process=True) + .. method:: set(value, process=True, severity=NO_ALARM, alarm=UDF_ALARM) - Updates the value associated with the record. By default this will + Updates the stored value and severity status. By default this will trigger record processing, and so will cause any associated `on_update` and `validate` methods to be called. If ``process`` is `False` then neither of these methods will be called, but the value will still be updated. + .. method:: set_alarm(severity, alarm) + + This is exactly equivalent to calling:: + + rec.set(rec.get(), severity=severity, alarm=alarm) + + and triggers an alarm status change without changing the value. + .. method:: get() Returns the value associated with the record. diff --git a/softioc/device.py b/softioc/device.py index 583eae35..0531cdbb 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -176,9 +176,15 @@ def __init__(self, name, **kargs): self.__enable_write = True if 'initial_value' in kargs: - self._value = self._value_to_epics(kargs.pop('initial_value')) + value = self._value_to_epics(kargs.pop('initial_value')) + initial_alarm = alarm.NO_ALARM else: - self._value = None + value = None + # To maintain backwards compatibility, if there is no initial value + # we mark the record as invalid + initial_alarm = alarm.INVALID_ALARM + + self._value = (value, initial_alarm, alarm.UDF_ALARM) self._blocking = kargs.pop('blocking', blocking) if self._blocking: @@ -190,18 +196,22 @@ def init_record(self, record): '''Special record initialisation for out records only: implements special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' - if self._value is None: + if self._value[0] is None: # Cannot set in __init__ (like we do for In records), as we want # the record alarm status to be set if no value was provided - # Probably related to PythonSoftIOC issue #53 - self._value = self._default_value() - else: - self._write_value(record, self._value) - if 'MLST' in self._fields_: - record.MLST = self._value - record.TIME = time.time() - record.UDF = 0 - recGblResetAlarms(record) + value = self._default_value() + self._value = (value, self._value[1], self._value[2]) + + self._write_value(record, self._value[0]) + if 'MLST' in self._fields_: + record.MLST = self._value[0] + + record.TIME = time.time() + + record.UDF = 0 + record.NSEV = self._value[1] + record.NSTA = self._value[2] + recGblResetAlarms(record) return self._epics_rc_ def __completion(self, record): @@ -216,9 +226,14 @@ def _process(self, record): if record.PACT: return EPICS_OK + # Ignore memoized value, retrieve it from the VAL field directly later + _, severity, alarm = self._value + + self.process_severity(record, severity, alarm) + value = self._read_value(record) if not self.__always_update and \ - self._compare_values(value, self._value): + self._compare_values(value, self._value[0]): # If the value isn't making a change then don't do anything. return EPICS_OK @@ -227,11 +242,11 @@ def _process(self, record): not self.__validate(self, python_value): # Asynchronous validation rejects value, so restore the last good # value. - self._write_value(record, self._value) + self._write_value(record, self._value[0]) return EPICS_ERROR else: # Value is good. Hang onto it, let users know the value has changed - self._value = value + self._value = (value, severity, alarm) record.UDF = 0 if self.__on_update and self.__enable_write: record.PACT = self._blocking @@ -248,15 +263,26 @@ def _value_to_dbr(self, value): return self._dbf_type_, 1, addressof(value), value - def set(self, value, process=True): + def set_alarm(self, severity, alarm): + '''Updates the alarm status without changing the stored value. An + update is triggered, and a timestamp can optionally be specified.''' + self._value = (self._value[0], severity, alarm) + self.set( + self.get(), + severity=severity, + alarm=alarm) + + + def set(self, value, process=True, + severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM): '''Special routine to set the value directly.''' value = self._value_to_epics(value) try: _record = self._record except AttributeError: - # Record not initialised yet. Record the value for when + # Record not initialised yet. Record data for when # initialisation occurs - self._value = value + self._value = (value, severity, alarm) else: # The array parameter is used to keep the raw pointer alive dbf_code, length, data, array = self._value_to_dbr(value) @@ -265,11 +291,11 @@ def set(self, value, process=True): self.__enable_write = True def get(self): - if self._value is None: + if self._value[0] is None: # Before startup complete if no value set return default value value = self._default_value() else: - value = self._value + value = self._value[0] return self._epics_to_value(value) diff --git a/tests/test_records.py b/tests/test_records.py index 1786b01e..3056e88d 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,9 +1,8 @@ import asyncio -import subprocess -import sys import numpy import os import pytest +from enum import Enum from conftest import ( aioca_cleanup, @@ -16,8 +15,8 @@ get_multiprocessing_context ) -from softioc import asyncio_dispatcher, builder, softioc -from softioc import alarm +from softioc import alarm, asyncio_dispatcher, builder, softioc +from softioc.builder import ClearRecords from softioc.device import SetBlocking from softioc.device_core import LookupRecord, LookupRecordList @@ -33,12 +32,14 @@ builder.mbbIn, builder.stringIn, builder.WaveformIn, + builder.longStringIn, ] def test_records(tmp_path): # Ensure we definitely unload all records that may be hanging over from # previous tests, then create exactly one instance of expected records. from sim_records import create_records + ClearRecords() create_records() path = str(tmp_path / "records.db") @@ -1215,3 +1216,112 @@ async def test_recursive_set(self): if process.exitcode is None: process.terminate() pytest.fail("Process did not finish cleanly, terminating") + +class TestAlarms: + """Tests related to record alarm status""" + + # Record creation function and associated PV name + records = [ + (builder.aIn, "AI_AlarmPV"), + (builder.boolIn, "BI_AlarmPV"), + (builder.longIn, "LI_AlarmPV"), + (builder.mbbIn, "MBBI_AlarmPV"), + (builder.stringIn, "SI_AlarmPV"), + (builder.WaveformIn, "WI_AlarmPV"), + (builder.longStringIn, "LSI_AlarmPV"), + (builder.aOut, "AO_AlarmPV"), + (builder.boolOut, "BO_AlarmPV"), + (builder.longOut, "LO_AlarmPV"), + (builder.stringOut, "SO_AlarmPV"), + (builder.mbbOut, "MBBO_AlarmPV"), + (builder.WaveformOut, "WO_AlarmPV"), + (builder.longStringOut, "LSO_AlarmPV"), + ] + + severity = alarm.INVALID_ALARM + status = alarm.DISABLE_ALARM + + class SetEnum(Enum): + """Enum to specify when set_alarm should be called""" + PRE_INIT = 0 + POST_INIT = 1 + + def alarm_test_func(self, device_name, conn, set_enum: SetEnum): + builder.SetDeviceName(device_name) + + pvs = [] + for record_func, name in self.records: + kwargs = {} + if record_func in [builder.WaveformOut, builder.WaveformIn]: + kwargs["length"] = WAVEFORM_LENGTH + + pvs.append(record_func(name, **kwargs)) + + if set_enum == self.SetEnum.PRE_INIT: + log("CHILD: Setting alarm before init") + for pv in pvs: + pv.set_alarm(self.severity, self.status) + + builder.LoadDatabase() + softioc.iocInit() + + if set_enum == self.SetEnum.POST_INIT: + log("CHILD: Setting alarm after init") + for pv in pvs: + pv.set_alarm(self.severity, self.status) + + conn.send("R") # "Ready" + log("CHILD: Sent R over Connection to Parent") + + # Keep process alive while main thread works. + while (True): + if conn.poll(TIMEOUT): + val = conn.recv() + if val == "D": # "Done" + break + + + @requires_cothread + @pytest.mark.parametrize("set_enum", [SetEnum.PRE_INIT, SetEnum.POST_INIT]) + def test_set_alarm_severity_status(self, set_enum): + """Test that set_alarm function allows setting severity and status""" + ctx = get_multiprocessing_context() + parent_conn, child_conn = ctx.Pipe() + + device_name = create_random_prefix() + + process = ctx.Process( + target=self.alarm_test_func, + args=(device_name, child_conn, set_enum), + ) + + process.start() + + from cothread.catools import caget, _channel_cache, FORMAT_CTRL + + try: + # Wait for message that IOC has started + select_and_recv(parent_conn, "R") + + # Suppress potential spurious warnings + _channel_cache.purge() + + for _, name in self.records: + + ret_val = caget( + device_name + ":" + name, + timeout=TIMEOUT, + format=FORMAT_CTRL + ) + + assert ret_val.severity == self.severity, \ + f"Severity mismatch for record {name}" + assert ret_val.status == self.status, \ + f"Status mismatch for record {name}" + + + finally: + # Suppress potential spurious warnings + _channel_cache.purge() + parent_conn.send("D") # "Done" + process.join(timeout=TIMEOUT) From a520028d7e717d55ef8f03b5407770682ff9b4d0 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 8 Apr 2024 13:42:37 +0100 Subject: [PATCH 06/10] Change default alarm values This ensures we only set INVALID_ALARM+UDF_ALARM when there is no value, but otherwise both the severity and the status are set to NO_ALARM --- docs/reference/api.rst | 4 ++-- softioc/device.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index ca916d4b..8873f752 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -550,7 +550,7 @@ class which provides the methods documented below. This class is used to implement Python device support for the record types ``ai``, ``bi``, ``longin``, ``mbbi`` and IN ``waveform`` records. - .. method:: set(value, severity=NO_ALARM, alarm=UDF_ALARM, timestamp=None) + .. method:: set(value, severity=NO_ALARM, alarm=NO_ALARM, timestamp=None) Updates the stored value and severity status and triggers an update. If ``SCAN`` has been set to ``'I/O Intr'`` (which is the default if the @@ -609,7 +609,7 @@ Working with OUT records ``ao``, ``bo``, ``longout``, ``mbbo`` and OUT ``waveform`` records. All OUT records support the following methods. - .. method:: set(value, process=True, severity=NO_ALARM, alarm=UDF_ALARM) + .. method:: set(value, process=True, severity=NO_ALARM, alarm=NO_ALARM) Updates the stored value and severity status. By default this will trigger record processing, and so will cause any associated `on_update` diff --git a/softioc/device.py b/softioc/device.py index 0531cdbb..076aed32 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -137,7 +137,7 @@ def _process(self, record): return self._epics_rc_ def set(self, value, - severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): + severity=alarm.NO_ALARM, alarm=alarm.NO_ALARM, timestamp=None): '''Updates the stored value and triggers an update. The alarm severity and timestamp can also be specified if appropriate.''' value = self._value_to_epics(value) @@ -177,14 +177,16 @@ def __init__(self, name, **kargs): if 'initial_value' in kargs: value = self._value_to_epics(kargs.pop('initial_value')) - initial_alarm = alarm.NO_ALARM + initial_severity = alarm.NO_ALARM + initial_status = alarm.NO_ALARM else: value = None # To maintain backwards compatibility, if there is no initial value # we mark the record as invalid - initial_alarm = alarm.INVALID_ALARM + initial_severity = alarm.INVALID_ALARM + initial_status = alarm.UDF_ALARM - self._value = (value, initial_alarm, alarm.UDF_ALARM) + self._value = (value, initial_severity, initial_status) self._blocking = kargs.pop('blocking', blocking) if self._blocking: @@ -274,7 +276,7 @@ def set_alarm(self, severity, alarm): def set(self, value, process=True, - severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM): + severity=alarm.NO_ALARM, alarm=alarm.NO_ALARM): '''Special routine to set the value directly.''' value = self._value_to_epics(value) try: From 9dd0fce99890de194d2f018a312d25e14391a1c9 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 8 Apr 2024 13:43:26 +0100 Subject: [PATCH 07/10] Add note that NO_ALARM is valid for status too --- softioc/alarm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/softioc/alarm.py b/softioc/alarm.py index e1366c3f..2e1895f9 100644 --- a/softioc/alarm.py +++ b/softioc/alarm.py @@ -5,6 +5,7 @@ INVALID_ALARM = 3 # Some alarm code definitions taken from EPICS alarm.h +# NO_ALARM = 0 READ_ALARM = 1 WRITE_ALARM = 2 HIHI_ALARM = 3 From a4730f0f1f1599b15a2ad95ee1332334b010d946 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 8 Apr 2024 13:56:19 +0100 Subject: [PATCH 08/10] Remove unnecessary delayed processing of value Previously we did this as we wanted to spot the case where no value was provided, either during record creation or by a set() call at some later time, so that we could mark the record as being in an invalid state. Now that we store the severity and status alongside the value, we don't need to handle this special case. --- softioc/device.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 076aed32..22fd4657 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -180,7 +180,7 @@ def __init__(self, name, **kargs): initial_severity = alarm.NO_ALARM initial_status = alarm.NO_ALARM else: - value = None + value = self._default_value() # To maintain backwards compatibility, if there is no initial value # we mark the record as invalid initial_severity = alarm.INVALID_ALARM @@ -198,11 +198,6 @@ def init_record(self, record): '''Special record initialisation for out records only: implements special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' - if self._value[0] is None: - # Cannot set in __init__ (like we do for In records), as we want - # the record alarm status to be set if no value was provided - value = self._default_value() - self._value = (value, self._value[1], self._value[2]) self._write_value(record, self._value[0]) if 'MLST' in self._fields_: From c2facee1b4b73a0ef2e3f2fd8e4d9152cfb98eed Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 9 Apr 2024 09:23:38 +0100 Subject: [PATCH 09/10] Clean up code after review --- softioc/device.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 22fd4657..4f4989f1 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -223,12 +223,11 @@ def _process(self, record): if record.PACT: return EPICS_OK - # Ignore memoized value, retrieve it from the VAL field directly later + # Ignore memoized value, retrieve it from the VAL field instead + value = self._read_value(record) _, severity, alarm = self._value - self.process_severity(record, severity, alarm) - value = self._read_value(record) if not self.__always_update and \ self._compare_values(value, self._value[0]): # If the value isn't making a change then don't do anything. @@ -288,12 +287,7 @@ def set(self, value, process=True, self.__enable_write = True def get(self): - if self._value[0] is None: - # Before startup complete if no value set return default value - value = self._default_value() - else: - value = self._value[0] - return self._epics_to_value(value) + return self._epics_to_value(self._value[0]) def _Device(Base, record_type, ctype, dbf_type, epics_rc, mlst = False): From d626eb42982c1e1df1dac5cc22e9518018976ba1 Mon Sep 17 00:00:00 2001 From: Xavier GOIZIOU Date: Mon, 15 Apr 2024 19:10:57 +0200 Subject: [PATCH 10/10] Update CHANGELOG.rst Added documentation for the non_interactive_ioc function. Co-authored-by: Alexander Wells <79699091+AlexanderWells-diamond@users.noreply.github.com> Update asyncio_dispatcher.py Update softioc.py Optimizing the wait_for_quit() function Modify signal declaration --- CHANGELOG.rst | 2 ++ docs/reference/api.rst | 6 ++++++ softioc/asyncio_dispatcher.py | 13 +++---------- softioc/softioc.py | 17 ++++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index be06782e..f7fece06 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,9 @@ Unreleased_ ----------- Added: + - `Enable setting alarm status of Out records <../../pull/157>`_ +- `Adding the non_interactive_ioc function <../../pull/156>`_ Removed: diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 8873f752..86f30e4b 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -120,6 +120,12 @@ Test Facilities`_ documentation for more details of each function. .. autofunction:: generalTimeReport .. autofunction:: eltc +.. autofunction:: non_interactive_ioc + +When used with a service manager, use python's -u option or the environment +variable PYTHONUNBUFFERED=TRUE. This ensures that python output, i.e. stdout +and stderr streams, is sent directly to the terminal. + .. attribute:: exit Displaying this value will invoke ``epicsExit()`` causing the IOC to diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index 5506d3fe..eeea73dc 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -50,20 +50,13 @@ def close(self): self.__shutdown() def wait_for_quit(self): - async_stop_event = asyncio.Event() stop_event = threading.Event() - def signal_exit(): - self.loop.call_soon_threadsafe(async_stop_event.set) - - async def async_signal_exit(): - await async_stop_event.wait() + def signal_handler(signum, frame): stop_event.set() - for sig in (signal.SIGINT, signal.SIGTERM): - signal.signal(sig, lambda signum, frame: signal_exit()) - - asyncio.run_coroutine_threadsafe(async_signal_exit(), self.loop) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) stop_event.wait() diff --git a/softioc/softioc.py b/softioc/softioc.py index c06e1c2f..117d09bd 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -354,15 +354,10 @@ def interactive_ioc(context = {}, call_exit = True): safeEpicsExit(0) def non_interactive_ioc(): - '''Launches IOC in non-interactive mode for background use - - When used with a service manager, use python's -u option or the environment - variable PYTHONUNBUFFERED=TRUE. - This ensures that python output, i.e. stdout and stderr streams, is sent - directly to the terminal. + '''Function to run the IOC in non-interactive mode. This mode is useful for + running the IOC as a background process without user interaction. + This function expects a stop signal. When it receives one, the IOC stops. ''' - if device.dispatcher: - device.dispatcher.wait_for_quit() - safeEpicsExit(0) - else: - print("No dispatcher found") + device.dispatcher.wait_for_quit() + safeEpicsExit(0) +