Skip to content

Commit ba24655

Browse files
Merge pull request #186 from DiamondLightSource/disable_pva
Allow starting IOC without PVA
2 parents e1f104a + 026ed69 commit ba24655

File tree

5 files changed

+118
-14
lines changed

5 files changed

+118
-14
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Added:
1616
- `Add int64In/Out record support <../../pull/161>`_
1717
- `Enable setting alarm status of Out records <../../pull/157>`_
1818
- `Adding the non_interactive_ioc function <../../pull/156>`_
19+
- `Allow starting IOC without PVA <../../pull/186>`_
1920

2021
Removed:
2122

softioc/__init__.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
'''Python soft IOC module.'''
22
import os
3-
import ctypes
43

5-
from setuptools_dso.runtime import find_dso
64
import epicscorelibs.path
7-
import pvxslibs.path
8-
from epicscorelibs.ioc import \
9-
iocshRegisterCommon, registerRecordDeviceDriver, pdbbase
5+
from epicscorelibs.ioc import iocshRegisterCommon, pdbbase
106

117
# Do this as early as possible, in case we happen to use cothread
128
# This will set the CATOOLS_LIBCA_PATH environment variable in case we use
@@ -15,20 +11,16 @@
1511

1612
# This import will also pull in the extension, which is needed
1713
# before we call iocshRegisterCommon
18-
from .imports import dbLoadDatabase
14+
from .imports import dbLoadDatabase, registerRecordDeviceDriver
1915
from ._version_git import __version__
2016

2117
# Need to do this before calling anything in device.py
2218
iocshRegisterCommon()
2319
base_dbd_path = os.path.join(epicscorelibs.path.base_path, 'dbd')
2420
dbLoadDatabase('base.dbd', base_dbd_path, None)
25-
dbLoadDatabase('pvxsIoc.dbd', pvxslibs.path.dbd_path, None)
2621
iocStats = os.path.join(os.path.dirname(__file__), "iocStats", "devIocStats")
2722
dbLoadDatabase('devIocStats.dbd', iocStats, None)
2823

29-
ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL)
30-
os.environ.setdefault('PVXS_QSRV_ENABLE', 'YES')
31-
if registerRecordDeviceDriver(pdbbase):
32-
raise RuntimeError('Error registering')
24+
registerRecordDeviceDriver(pdbbase)
3325

3426
__all__ = ['__version__']

softioc/imports.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def from_param(cls, value):
105105
epicsExitCallAtExits.argtypes = ()
106106
epicsExitCallAtExits.restype = None
107107

108+
registerRecordDeviceDriver = dbCore.registerAllRecordDeviceDrivers
109+
registerRecordDeviceDriver.argtypes = (c_void_p,)
110+
registerRecordDeviceDriver.errcheck = expect_success
108111

109112
__all__ = [
110113
'get_field_offsets',

softioc/softioc.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import ctypes
12
import os
23
import sys
34
import atexit
45
from ctypes import *
56
from tempfile import NamedTemporaryFile
67

8+
import pvxslibs.path
9+
from epicscorelibs.ioc import pdbbase
10+
from setuptools_dso.runtime import find_dso
11+
712
from . import autosave, imports, device
813
from . import cothread_dispatcher
914

@@ -16,14 +21,15 @@ def epicsAtPyExit():
1621
imports.epicsExitCallAtExits()
1722

1823

19-
def iocInit(dispatcher=None):
24+
def iocInit(dispatcher=None, enable_pva=True):
2025
'''This must be called exactly once after loading all EPICS database files.
2126
After this point the EPICS IOC is running and serving PVs.
2227
2328
Args:
2429
dispatcher: A callable with signature ``dispatcher(func, *args)``. Will
25-
be called in response to caput on a record. If not supplied use
26-
`cothread` as a dispatcher.
30+
be called in response to caput on a record. If not supplied uses
31+
``cothread`` as the dispatcher.
32+
enable_pva: Specify whether to enable the PV Access Server in this IOC.
2733
2834
See Also:
2935
`softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications
@@ -33,6 +39,13 @@ def iocInit(dispatcher=None):
3339
dispatcher = cothread_dispatcher.CothreadDispatcher()
3440
# Set the dispatcher for record processing callbacks
3541
device.dispatcher = dispatcher
42+
43+
if enable_pva:
44+
dbLoadDatabase('pvxsIoc.dbd', pvxslibs.path.dbd_path, None)
45+
ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL)
46+
47+
imports.registerRecordDeviceDriver(pdbbase)
48+
3649
imports.iocInit()
3750
autosave.start_autosave_thread()
3851

tests/test_pvaccess.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import asyncio
2+
from contextlib import nullcontext
3+
4+
import pytest
5+
6+
from conftest import (
7+
aioca_cleanup,
8+
log,
9+
create_random_prefix,
10+
TIMEOUT,
11+
select_and_recv,
12+
get_multiprocessing_context,
13+
)
14+
15+
from softioc import asyncio_dispatcher, builder, softioc
16+
17+
18+
class TestPVAccess:
19+
"""Tests related to PVAccess"""
20+
21+
record_name = "PVA_AOut"
22+
record_value = 10
23+
24+
def pva_test_func(self, device_name, conn, use_pva):
25+
builder.SetDeviceName(device_name)
26+
27+
builder.aOut(self.record_name, initial_value=self.record_value)
28+
29+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
30+
builder.LoadDatabase()
31+
softioc.iocInit(dispatcher=dispatcher, enable_pva=use_pva)
32+
33+
conn.send("R") # "Ready"
34+
log("CHILD: Sent R over Connection to Parent")
35+
36+
# Keep process alive while main thread works.
37+
while (True):
38+
if conn.poll(TIMEOUT):
39+
val = conn.recv()
40+
if val == "D": # "Done"
41+
break
42+
43+
@pytest.mark.asyncio
44+
@pytest.mark.parametrize(
45+
"use_pva,expectation",
46+
[
47+
(True, nullcontext()),
48+
(False, pytest.raises(asyncio.TimeoutError))
49+
]
50+
)
51+
async def test_pva_enable_disable(self, use_pva, expectation):
52+
"""Test that we can enable and disable PVA, perform PVAccess requests
53+
when enabled, and that we can always do Channel Access requests"""
54+
ctx = get_multiprocessing_context()
55+
parent_conn, child_conn = ctx.Pipe()
56+
57+
device_name = create_random_prefix()
58+
59+
process = ctx.Process(
60+
target=self.pva_test_func,
61+
args=(device_name, child_conn, use_pva),
62+
)
63+
64+
process.start()
65+
66+
from aioca import caget
67+
from p4p.client.asyncio import Context
68+
try:
69+
# Wait for message that IOC has started
70+
select_and_recv(parent_conn, "R")
71+
72+
record_full_name = device_name + ":" + self.record_name
73+
74+
ret_val = await caget(record_full_name, timeout=TIMEOUT)
75+
76+
assert ret_val == self.record_value
77+
78+
with expectation as _:
79+
with Context("pva") as ctx:
80+
# Short timeout as, if the above CA connection has happened
81+
# there's no need to wait a very long time for the PVA
82+
# connection
83+
pva_val = await asyncio.wait_for(
84+
ctx.get(record_full_name),
85+
timeout=2
86+
)
87+
assert pva_val == self.record_value
88+
89+
90+
finally:
91+
# Clear the cache before stopping the IOC stops
92+
# "channel disconnected" error messages
93+
aioca_cleanup()
94+
parent_conn.send("D") # "Done"
95+
process.join(timeout=TIMEOUT)

0 commit comments

Comments
 (0)