Skip to content

Commit 0dca326

Browse files
Add get_field and set_field methods to records
These allow access to all EPICS attributes on a record. Also add documentation and tests
1 parent dad702c commit 0dca326

5 files changed

Lines changed: 278 additions & 0 deletions

File tree

‎docs/reference/api.rst‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,27 @@ class which provides the methods documented below.
578578
Note that channel access puts to a Python soft IOC input record are
579579
completely ineffective, and this includes waveform records.
580580

581+
.. method:: get_field(field)
582+
583+
This returns the named field from the record. An exception will be raised
584+
if the field cannot be found.
585+
586+
Note that this function can only be used after the IOC has been initialized.
587+
If you need to retrieve a field's value before that, access it directly via
588+
an attribute e.g. ``my_record.EGU``. (This will not work after the IOC is
589+
initialized)
590+
591+
.. method:: set_field(field, value)
592+
593+
This sets the given field to the given value. The value will
594+
always be converted to a Python String, which is then interpreted by
595+
EPICS as a DBF_STRING type. Note that values can be no longer than 39 bytes.
596+
597+
Note that this function can only be used after the IOC has been initialized.
598+
If you need to set a field's value before that, set it directly as an attribute
599+
on the record e.g. ``my_record.EGU``. (This will not work after the IOC is
600+
initialized)
601+
581602
Working with OUT records
582603
~~~~~~~~~~~~~~~~~~~~~~~~
583604

@@ -599,4 +620,25 @@ Working with OUT records
599620

600621
Returns the value associated with the record.
601622

623+
.. method:: get_field(field)
624+
625+
This returns the named field from the record. An exception will be raised
626+
if the field cannot be found.
627+
628+
Note that this function can only be used after the IOC has been initialized.
629+
If you need to retrieve a field's value before that, access it directly via
630+
an attribute e.g. ``my_record.EGU``. (This will not work after the IOC is
631+
initialized)
632+
633+
.. method:: set_field(field, value)
634+
635+
This sets the given field to the given value. The value will
636+
always be converted to a Python String, which is then interpreted by
637+
EPICS as a DBF_STRING type. Note that values can be no longer than 39 bytes.
638+
639+
Note that this function can only be used after the IOC has been initialized.
640+
If you need to set a field's value before that, set it directly as an attribute
641+
on the record e.g. ``my_record.EGU``. (This will not work after the IOC is
642+
initialized)
643+
602644
.. _epics_device: https://github.com/Araneidae/epics_device

‎softioc/device.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
signal_processing_complete,
1313
recGblResetAlarms,
1414
db_put_field,
15+
db_get_field,
1516
)
1617
from .device_core import DeviceSupportCore, RecordLookup
1718

@@ -83,6 +84,22 @@ def _read_value(self, record):
8384
def _write_value(self, record, value):
8485
record.write_val(value)
8586

87+
def set_field(self, field, value):
88+
'''Sets the given field to the given value. Value will be transported as
89+
a DBF_STRING.'''
90+
assert hasattr(self, "_record"), 'set_field may only be called after iocInit'
91+
data = (c_char * 40)()
92+
data.value = str(value).encode() + b'\0'
93+
name = self._name + '.' + field
94+
db_put_field(name, fields.DBF_STRING, addressof(data), 1)
95+
96+
def get_field(self, field):
97+
''' Returns the given field value as a string.'''
98+
assert hasattr(self, "_record"), 'get_field may only be called after iocInit'
99+
data = (c_char * 40)()
100+
name = self._name + '.' + field
101+
db_get_field(name, fields.DBF_STRING, addressof(data), 1)
102+
return _string_at(data, 40)
86103

87104
class ProcessDeviceSupportIn(ProcessDeviceSupportCore):
88105
_link_ = 'INP'

‎softioc/extension.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ static PyObject *db_put_field(PyObject *self, PyObject *args)
113113
Py_RETURN_NONE;
114114
}
115115

116+
static PyObject *db_get_field(PyObject *self, PyObject *args)
117+
{
118+
const char *name;
119+
short dbrType;
120+
void *pbuffer;
121+
long length;
122+
if (!PyArg_ParseTuple(args, "shnl", &name, &dbrType, &pbuffer, &length))
123+
return NULL;
124+
125+
long options = 0;
126+
struct dbAddr dbAddr;
127+
if (dbNameToAddr(name, &dbAddr))
128+
return PyErr_Format(
129+
PyExc_RuntimeError, "dbNameToAddr failed for %s", name);
130+
if (dbGetField(&dbAddr, dbrType, pbuffer, &options, &length, NULL))
131+
return PyErr_Format(
132+
PyExc_RuntimeError, "dbGetField failed for %s", name);
133+
Py_RETURN_NONE;
134+
}
116135

117136
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
118137
/* IOC PV put logging */
@@ -266,6 +285,8 @@ static struct PyMethodDef softioc_methods[] = {
266285
"Get offset, size and type for each record field"},
267286
{"db_put_field", db_put_field, METH_VARARGS,
268287
"Put a database field to a value"},
288+
{"db_get_field", db_get_field, METH_VARARGS,
289+
"Get a database field's value"},
269290
{"install_pv_logging", install_pv_logging, METH_VARARGS,
270291
"Install caput logging to stdout"},
271292
{"signal_processing_complete", signal_processing_complete, METH_VARARGS,

‎softioc/imports.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def db_put_field(name, dbr_type, pbuffer, length):
2323
'''Put field where pbuffer is void* pointer. Returns RC'''
2424
return _extension.db_put_field(name, dbr_type, pbuffer, length)
2525

26+
def db_get_field(name, dbr_type, pbuffer, length):
27+
'''Get field where pbuffer is void* pointer. Returns Py_RETURN_NONE'''
28+
return _extension.db_get_field(name, dbr_type, pbuffer, length)
29+
2630
def install_pv_logging(acf_file):
2731
'''Install pv logging'''
2832
_extension.install_pv_logging(acf_file)

‎tests/test_records.py‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,3 +904,197 @@ async def query_record(index):
904904
log(f"PARENT: Join completed with exitcode {process.exitcode}")
905905
if process.exitcode is None:
906906
pytest.fail("Process did not terminate")
907+
908+
class TestGetSetField:
909+
"""Tests related to get_field and set_field on records"""
910+
911+
test_result_rec = "TestResult"
912+
913+
def test_set_field_before_init_fails(self):
914+
"""Test that calling set_field before iocInit() raises an exception"""
915+
916+
ao = builder.aOut("testAOut")
917+
918+
with pytest.raises(AssertionError) as e:
919+
ao.set_field("EGU", "Deg")
920+
921+
assert "set_field may only be called after iocInit" in str(e.value)
922+
923+
def test_get_field_before_init_fails(self):
924+
"""Test that calling get_field before iocInit() raises an exception"""
925+
926+
ao = builder.aOut("testAOut")
927+
928+
with pytest.raises(AssertionError) as e:
929+
ao.get_field("EGU")
930+
931+
assert "get_field may only be called after iocInit" in str(e.value)
932+
933+
def get_set_test_func(self, device_name, conn):
934+
"""Run an IOC and do simple get_field/set_field calls"""
935+
936+
builder.SetDeviceName(device_name)
937+
938+
lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12)
939+
940+
# Record to indicate success/failure
941+
bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS")
942+
943+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
944+
builder.LoadDatabase()
945+
softioc.iocInit(dispatcher)
946+
947+
conn.send("R") # "Ready"
948+
949+
log("CHILD: Sent R over Connection to Parent")
950+
951+
# Set and then get the EGU field
952+
egu = "TEST"
953+
lo.set_field("EGU", egu)
954+
log("CHILD: set_field successful")
955+
readback_egu = lo.get_field("EGU")
956+
log(f"CHILD: get_field returned {readback_egu}")
957+
assert readback_egu == egu, f"EGU field was not {egu}, was {readback_egu}"
958+
log(f"CHILD: assert passed")
959+
960+
# Test completed, report to listening camonitor
961+
bi.set(True)
962+
963+
# Keep process alive while main thread works.
964+
while (True):
965+
if conn.poll(TIMEOUT):
966+
val = conn.recv()
967+
if val == "D": # "Done"
968+
break
969+
970+
log("CHILD: Received exit command, child exiting")
971+
972+
973+
@pytest.mark.asyncio
974+
async def test_get_set(self):
975+
"""Test a simple set_field/get_field is successful"""
976+
ctx = get_multiprocessing_context()
977+
parent_conn, child_conn = ctx.Pipe()
978+
979+
device_name = create_random_prefix()
980+
981+
process = ctx.Process(
982+
target=self.get_set_test_func,
983+
args=(device_name, child_conn),
984+
)
985+
986+
process.start()
987+
988+
log("PARENT: Child started, waiting for R command")
989+
990+
from aioca import camonitor
991+
992+
try:
993+
# Wait for message that IOC has started
994+
select_and_recv(parent_conn, "R")
995+
996+
log("PARENT: received R command")
997+
998+
queue = asyncio.Queue()
999+
record = device_name + ":" + self.test_result_rec
1000+
monitor = camonitor(record, queue.put)
1001+
1002+
log(f"PARENT: monitoring {record}")
1003+
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
1004+
log(f"PARENT: new_val is {new_val}")
1005+
assert new_val == True, "Test failed, value was not True"
1006+
1007+
finally:
1008+
monitor.close()
1009+
# Clear the cache before stopping the IOC stops
1010+
# "channel disconnected" error messages
1011+
aioca_cleanup()
1012+
1013+
log("PARENT: Sending Done command to child")
1014+
parent_conn.send("D") # "Done"
1015+
process.join(timeout=TIMEOUT)
1016+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1017+
if process.exitcode is None:
1018+
pytest.fail("Process did not terminate")
1019+
1020+
def get_set_too_long_value(self, device_name, conn):
1021+
"""Run an IOC and deliberately call set_field with a too-long value"""
1022+
1023+
builder.SetDeviceName(device_name)
1024+
1025+
lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12)
1026+
1027+
# Record to indicate success/failure
1028+
bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS")
1029+
1030+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
1031+
builder.LoadDatabase()
1032+
softioc.iocInit(dispatcher)
1033+
1034+
conn.send("R") # "Ready"
1035+
1036+
log("CHILD: Sent R over Connection to Parent")
1037+
1038+
# Set a too-long value and confirm it reports an error
1039+
try:
1040+
lo.set_field("EGU", "ThisStringIsFarTooLongToFitIntoTheEguField")
1041+
except ValueError as e:
1042+
# Expected error, report success to listening camonitor
1043+
assert "byte string too long" in e.args[0]
1044+
bi.set(True)
1045+
1046+
# Keep process alive while main thread works.
1047+
while (True):
1048+
if conn.poll(TIMEOUT):
1049+
val = conn.recv()
1050+
if val == "D": # "Done"
1051+
break
1052+
1053+
log("CHILD: Received exit command, child exiting")
1054+
1055+
@pytest.mark.asyncio
1056+
async def test_set_too_long_value(self):
1057+
"""Test that set_field with a too-long value raises the expected error"""
1058+
ctx = get_multiprocessing_context()
1059+
parent_conn, child_conn = ctx.Pipe()
1060+
1061+
device_name = create_random_prefix()
1062+
1063+
process = ctx.Process(
1064+
target=self.get_set_too_long_value,
1065+
args=(device_name, child_conn),
1066+
)
1067+
1068+
process.start()
1069+
1070+
log("PARENT: Child started, waiting for R command")
1071+
1072+
from aioca import camonitor
1073+
1074+
try:
1075+
# Wait for message that IOC has started
1076+
select_and_recv(parent_conn, "R")
1077+
1078+
log("PARENT: received R command")
1079+
1080+
queue = asyncio.Queue()
1081+
record = device_name + ":" + self.test_result_rec
1082+
monitor = camonitor(record, queue.put)
1083+
1084+
log(f"PARENT: monitoring {record}")
1085+
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
1086+
log(f"PARENT: new_val is {new_val}")
1087+
assert new_val == True, "Test failed, value was not True"
1088+
1089+
finally:
1090+
monitor.close()
1091+
# Clear the cache before stopping the IOC stops
1092+
# "channel disconnected" error messages
1093+
aioca_cleanup()
1094+
1095+
log("PARENT: Sending Done command to child")
1096+
parent_conn.send("D") # "Done"
1097+
process.join(timeout=TIMEOUT)
1098+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1099+
if process.exitcode is None:
1100+
pytest.fail("Process did not terminate")

0 commit comments

Comments
 (0)