softioc
Advanced tools
| class CothreadDispatcher: | ||
| def __init__(self, dispatcher = None): | ||
| """A dispatcher for `cothread` based IOCs, suitable to be passed to | ||
| `softioc.iocInit`. By default scheduled tasks are run on a dedicated | ||
| cothread callback thread, but an alternative dispatcher can optionally | ||
| be specified here. Realistically the only sensible alternative is to | ||
| pass `cothread.Spawn`, which would create a separate cothread for each | ||
| dispatched callback. | ||
| """ | ||
| if dispatcher is None: | ||
| # Import here to ensure we don't instantiate any of cothread's | ||
| # global state unless we have to | ||
| import cothread | ||
| # Create our own cothread callback queue so that our callbacks | ||
| # processing doesn't interfere with other callback processing. | ||
| self.__dispatcher = cothread.cothread._Callback() | ||
| else: | ||
| self.__dispatcher = dispatcher | ||
| def __call__( | ||
| self, | ||
| func, | ||
| func_args=(), | ||
| completion = None, | ||
| completion_args=()): | ||
| def wrapper(): | ||
| func(*func_args) | ||
| if completion: | ||
| completion(*completion_args) | ||
| self.__dispatcher(wrapper) |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: softioc | ||
| Version: 4.0.2 | ||
| Version: 4.1.0 | ||
| Summary: Embed an EPICS IOC in a Python process | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/dls-controls/pythonSoftIOC |
+1
-1
@@ -61,3 +61,3 @@ [metadata] | ||
| addopts = | ||
| --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs --ignore=docs | ||
| --tb=native -vv --doctest-modules --ignore=softioc/iocStats --ignore=epicscorelibs --ignore=docs | ||
| --cov=softioc --cov-report term --cov-report xml:cov.xml | ||
@@ -64,0 +64,0 @@ asyncio_mode = auto |
| Metadata-Version: 2.1 | ||
| Name: softioc | ||
| Version: 4.0.2 | ||
| Version: 4.1.0 | ||
| Summary: Embed an EPICS IOC in a Python process | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/dls-controls/pythonSoftIOC |
@@ -13,2 +13,3 @@ LICENSE | ||
| softioc/builder.py | ||
| softioc/cothread_dispatcher.py | ||
| softioc/device.dbd | ||
@@ -15,0 +16,0 @@ softioc/device.py |
@@ -11,4 +11,4 @@ # Compute a version number from a git repo or archive | ||
| # These will be filled in if git archive is run or by setup.py cmdclasses | ||
| GIT_REFS = 'tag: 4.0.2' | ||
| GIT_SHA1 = '1eb9405' | ||
| GIT_REFS = 'tag: 4.1.0' | ||
| GIT_SHA1 = '77ec950' | ||
@@ -15,0 +15,0 @@ # Git describe gives us sha1, last version-like tag, and commits since then |
@@ -11,6 +11,7 @@ import asyncio | ||
| `softioc.iocInit`. Means that `on_update` callback functions can be | ||
| async. If loop is None, will run an Event Loop in a thread when created. | ||
| async. | ||
| If a ``loop`` is provided it must already be running. Otherwise a new | ||
| Event Loop will be created and run in a dedicated thread. | ||
| """ | ||
| #: `asyncio` event loop that the callbacks will run under. | ||
| self.loop = loop | ||
| if loop is None: | ||
@@ -30,11 +31,22 @@ # Make one and run it in a background thread | ||
| worker.start() | ||
| elif not loop.is_running(): | ||
| raise ValueError("Provided asyncio event loop is not running") | ||
| else: | ||
| self.loop = loop | ||
| def __call__(self, func, *args): | ||
| def __call__( | ||
| self, | ||
| func, | ||
| func_args=(), | ||
| completion = None, | ||
| completion_args=()): | ||
| async def async_wrapper(): | ||
| try: | ||
| ret = func(*args) | ||
| ret = func(*func_args) | ||
| if inspect.isawaitable(ret): | ||
| await ret | ||
| if completion: | ||
| completion(*completion_args) | ||
| except Exception: | ||
| logging.exception("Exception when awaiting callback") | ||
| logging.exception("Exception when running dispatched callback") | ||
| asyncio.run_coroutine_threadsafe(async_wrapper(), self.loop) |
@@ -11,2 +11,4 @@ import os | ||
| from . import device, pythonSoftIoc # noqa | ||
| # Re-export this so users only have to import the builder | ||
| from .device import SetBlocking # noqa | ||
@@ -305,3 +307,5 @@ PythonDevice = pythonSoftIoc.PythonDevice() | ||
| 'LoadDatabase', | ||
| 'SetDeviceName', 'UnsetDevice' | ||
| 'SetDeviceName', 'UnsetDevice', | ||
| # Device support functions | ||
| 'SetBlocking' | ||
| ] |
+39
-3
@@ -9,3 +9,9 @@ import os | ||
| from . import fields | ||
| from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field | ||
| from .imports import ( | ||
| create_callback_capsule, | ||
| dbLoadDatabase, | ||
| signal_processing_complete, | ||
| recGblResetAlarms, | ||
| db_put_field, | ||
| ) | ||
| from .device_core import DeviceSupportCore, RecordLookup | ||
@@ -15,6 +21,17 @@ | ||
| # This is set from softioc.iocInit | ||
| # dispatcher(func, *args) will queue a callback to happen | ||
| dispatcher = None | ||
| # Global blocking flag, used to mark asynchronous (False) or synchronous (True) | ||
| # processing modes for Out records. | ||
| # Default False to maintain behaviour from previous versions. | ||
| blocking = False | ||
| # Set the current global blocking flag, and return the previous value. | ||
| def SetBlocking(new_val): | ||
| global blocking | ||
| old_val = blocking | ||
| blocking = new_val | ||
| return old_val | ||
| # EPICS processing return codes | ||
@@ -147,2 +164,6 @@ EPICS_OK = 0 | ||
| self._blocking = kargs.pop('blocking', blocking) | ||
| if self._blocking: | ||
| self._callback = create_callback_capsule() | ||
| self.__super.__init__(name, **kargs) | ||
@@ -168,5 +189,14 @@ | ||
| def __completion(self, record): | ||
| '''Signals that all on_update processing is finished''' | ||
| if self._blocking: | ||
| signal_processing_complete(record, self._callback) | ||
| def _process(self, record): | ||
| '''Processing suitable for output records. Performs immediate value | ||
| validation and asynchronous update notification.''' | ||
| if record.PACT: | ||
| return EPICS_OK | ||
| value = self._read_value(record) | ||
@@ -190,3 +220,9 @@ if not self.__always_update and \ | ||
| if self.__on_update and self.__enable_write: | ||
| dispatcher(self.__on_update, python_value) | ||
| record.PACT = self._blocking | ||
| dispatcher( | ||
| self.__on_update, | ||
| func_args=(python_value,), | ||
| completion = self.__completion, | ||
| completion_args=(record,)) | ||
| return EPICS_OK | ||
@@ -193,0 +229,0 @@ |
+58
-3
@@ -1,2 +0,2 @@ | ||
| /* Provide EPICS functions in Python format */ | ||
| #define PY_SSIZE_T_CLEAN | ||
@@ -9,2 +9,3 @@ #include <Python.h> | ||
| #include <dbFldTypes.h> | ||
| #include <callback.h> | ||
| #include <dbStaticLib.h> | ||
@@ -17,2 +18,6 @@ #include <asTrapWrite.h> | ||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| /* Field access helper functions. */ | ||
| /* Reference stealing version of PyDict_SetItemString */ | ||
@@ -115,4 +120,3 @@ static void set_dict_item_steal( | ||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| /* IOC PV put logging */ | ||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| /* IOC PV put logging */ | ||
@@ -125,2 +129,3 @@ struct formatted | ||
| static struct formatted * FormatValue(struct dbAddr *dbaddr) | ||
@@ -161,2 +166,3 @@ { | ||
| static void PrintValue(struct formatted *formatted) | ||
@@ -178,2 +184,3 @@ { | ||
| void EpicsPvPutHook(struct asTrapWriteMessage *pmessage, int after) | ||
@@ -218,2 +225,45 @@ { | ||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| /* Process callback support. */ | ||
| #define CAPSULE_NAME "ProcessDeviceSupportOut.callback" | ||
| static void capsule_destructor(PyObject *obj) | ||
| { | ||
| free(PyCapsule_GetPointer(obj, CAPSULE_NAME)); | ||
| } | ||
| static PyObject *create_callback_capsule(PyObject *self, PyObject *args) | ||
| { | ||
| void *callback = malloc(sizeof(CALLBACK)); | ||
| return PyCapsule_New(callback, CAPSULE_NAME, &capsule_destructor); | ||
| } | ||
| static PyObject *signal_processing_complete(PyObject *self, PyObject *args) | ||
| { | ||
| int priority; | ||
| dbCommon *record; | ||
| PyObject *callback_capsule; | ||
| if (!PyArg_ParseTuple(args, "inO", &priority, &record, &callback_capsule)) | ||
| return NULL; | ||
| if (!PyCapsule_IsValid(callback_capsule, CAPSULE_NAME)) | ||
| return PyErr_Format( | ||
| PyExc_TypeError, | ||
| "Given object was not a capsule with name \"%s\"", | ||
| CAPSULE_NAME); | ||
| CALLBACK *callback = PyCapsule_GetPointer(callback_capsule, CAPSULE_NAME); | ||
| callbackRequestProcessCallback(callback, priority, record); | ||
| Py_RETURN_NONE; | ||
| } | ||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| /* Initialisation. */ | ||
| static struct PyMethodDef softioc_methods[] = { | ||
@@ -228,2 +278,6 @@ {"get_DBF_values", get_DBF_values, METH_VARARGS, | ||
| "Install caput logging to stdout"}, | ||
| {"signal_processing_complete", signal_processing_complete, METH_VARARGS, | ||
| "Inform EPICS that asynchronous record processing has completed"}, | ||
| {"create_callback_capsule", create_callback_capsule, METH_VARARGS, | ||
| "Create a CALLBACK structure inside a PyCapsule"}, | ||
| {NULL, NULL, 0, NULL} /* Sentinel */ | ||
@@ -241,2 +295,3 @@ }; | ||
| PyObject *PyInit__extension(void) | ||
@@ -243,0 +298,0 @@ { |
+12
-0
@@ -30,2 +30,12 @@ '''External DLL imports used for implementing Python EPICS device support. | ||
| def create_callback_capsule(): | ||
| return _extension.create_callback_capsule() | ||
| def signal_processing_complete(record, callback): | ||
| '''Signal that asynchronous record processing has completed''' | ||
| _extension.signal_processing_complete( | ||
| record.PRIO, | ||
| record.record.value, | ||
| callback) | ||
| def expect_success(status, function, args): | ||
@@ -98,2 +108,4 @@ assert status == 0, 'Expected success' | ||
| 'get_field_offsets', | ||
| 'create_callback_capsule', | ||
| 'signal_processing_complete', | ||
| 'registryDeviceSupportAdd', | ||
@@ -100,0 +112,0 @@ 'IOSCANPVT', 'scanIoRequest', 'scanIoInit', |
@@ -27,3 +27,3 @@ '''Device support files for pythonSoftIoc Python EPICS device support. | ||
| 'on_update', 'on_update_name', 'validate', 'always_update', | ||
| 'initial_value', '_wf_nelm', '_wf_dtype'] | ||
| 'initial_value', '_wf_nelm', '_wf_dtype', 'blocking'] | ||
| device_kargs = {} | ||
@@ -30,0 +30,0 @@ for keyword in DeviceKeywords: |
@@ -10,2 +10,3 @@ import os | ||
| from . import imports, device | ||
| from . import cothread_dispatcher | ||
@@ -35,6 +36,3 @@ __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] | ||
| # Fallback to cothread | ||
| import cothread | ||
| # Create our own cothread callback queue so that our callbacks | ||
| # processing doesn't interfere with other callback processing. | ||
| dispatcher = cothread.cothread._Callback() | ||
| dispatcher = cothread_dispatcher.CothreadDispatcher() | ||
| # Set the dispatcher for record processing callbacks | ||
@@ -41,0 +39,0 @@ device.dispatcher = dispatcher |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
265450
1.75%82
1.23%1665
5.11%