Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a9fa7f7

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 a9fa7f7

File tree

6 files changed

+289
-1
lines changed

6 files changed

+289
-1
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Versioning <https://semver.org/spec/v2.0.0.html>`_.
1010
Unreleased_
1111
-----------
1212

13-
Nothing yet
13+
- 'Add get_field and set_field methods to records <../../pull/140>'_
1414

1515
4.4.0_ - 2023-07-06
1616
-------------------

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: 21 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,26 @@ def _read_value(self, record):
8384
def _write_value(self, record, value):
8485
record.write_val(value)
8586

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

87108
class ProcessDeviceSupportIn(ProcessDeviceSupportCore):
88109
_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: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,3 +904,203 @@ 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, \
958+
f"EGU field was not {egu}, was {readback_egu}"
959+
960+
log("CHILD: assert passed")
961+
962+
# Test completed, report to listening camonitor
963+
bi.set(True)
964+
965+
# Keep process alive while main thread works.
966+
while (True):
967+
if conn.poll(TIMEOUT):
968+
val = conn.recv()
969+
if val == "D": # "Done"
970+
break
971+
972+
log("CHILD: Received exit command, child exiting")
973+
974+
975+
@pytest.mark.asyncio
976+
async def test_get_set(self):
977+
"""Test a simple set_field/get_field is successful"""
978+
ctx = get_multiprocessing_context()
979+
parent_conn, child_conn = ctx.Pipe()
980+
981+
device_name = create_random_prefix()
982+
983+
process = ctx.Process(
984+
target=self.get_set_test_func,
985+
args=(device_name, child_conn),
986+
)
987+
988+
process.start()
989+
990+
log("PARENT: Child started, waiting for R command")
991+
992+
from aioca import camonitor
993+
994+
try:
995+
# Wait for message that IOC has started
996+
select_and_recv(parent_conn, "R")
997+
998+
log("PARENT: received R command")
999+
1000+
queue = asyncio.Queue()
1001+
record = device_name + ":" + self.test_result_rec
1002+
monitor = camonitor(record, queue.put)
1003+
1004+
log(f"PARENT: monitoring {record}")
1005+
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
1006+
log(f"PARENT: new_val is {new_val}")
1007+
assert new_val == 1, \
1008+
f"Test failed, value was not 1(True), was {new_val}"
1009+
1010+
1011+
finally:
1012+
monitor.close()
1013+
# Clear the cache before stopping the IOC stops
1014+
# "channel disconnected" error messages
1015+
aioca_cleanup()
1016+
1017+
log("PARENT: Sending Done command to child")
1018+
parent_conn.send("D") # "Done"
1019+
process.join(timeout=TIMEOUT)
1020+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1021+
if process.exitcode is None:
1022+
pytest.fail("Process did not terminate")
1023+
1024+
def get_set_too_long_value(self, device_name, conn):
1025+
"""Run an IOC and deliberately call set_field with a too-long value"""
1026+
1027+
builder.SetDeviceName(device_name)
1028+
1029+
lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12)
1030+
1031+
# Record to indicate success/failure
1032+
bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS")
1033+
1034+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
1035+
builder.LoadDatabase()
1036+
softioc.iocInit(dispatcher)
1037+
1038+
conn.send("R") # "Ready"
1039+
1040+
log("CHILD: Sent R over Connection to Parent")
1041+
1042+
# Set a too-long value and confirm it reports an error
1043+
try:
1044+
lo.set_field("EGU", "ThisStringIsFarTooLongToFitIntoTheEguField")
1045+
except ValueError as e:
1046+
# Expected error, report success to listening camonitor
1047+
assert "byte string too long" in e.args[0]
1048+
bi.set(True)
1049+
1050+
# Keep process alive while main thread works.
1051+
while (True):
1052+
if conn.poll(TIMEOUT):
1053+
val = conn.recv()
1054+
if val == "D": # "Done"
1055+
break
1056+
1057+
log("CHILD: Received exit command, child exiting")
1058+
1059+
@pytest.mark.asyncio
1060+
async def test_set_too_long_value(self):
1061+
"""Test that set_field with a too-long value raises the expected
1062+
error"""
1063+
ctx = get_multiprocessing_context()
1064+
parent_conn, child_conn = ctx.Pipe()
1065+
1066+
device_name = create_random_prefix()
1067+
1068+
process = ctx.Process(
1069+
target=self.get_set_too_long_value,
1070+
args=(device_name, child_conn),
1071+
)
1072+
1073+
process.start()
1074+
1075+
log("PARENT: Child started, waiting for R command")
1076+
1077+
from aioca import camonitor
1078+
1079+
try:
1080+
# Wait for message that IOC has started
1081+
select_and_recv(parent_conn, "R")
1082+
1083+
log("PARENT: received R command")
1084+
1085+
queue = asyncio.Queue()
1086+
record = device_name + ":" + self.test_result_rec
1087+
monitor = camonitor(record, queue.put)
1088+
1089+
log(f"PARENT: monitoring {record}")
1090+
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
1091+
log(f"PARENT: new_val is {new_val}")
1092+
assert new_val == 1, \
1093+
f"Test failed, value was not 1(True), was {new_val}"
1094+
1095+
finally:
1096+
monitor.close()
1097+
# Clear the cache before stopping the IOC stops
1098+
# "channel disconnected" error messages
1099+
aioca_cleanup()
1100+
1101+
log("PARENT: Sending Done command to child")
1102+
parent_conn.send("D") # "Done"
1103+
process.join(timeout=TIMEOUT)
1104+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1105+
if process.exitcode is None:
1106+
pytest.fail("Process did not terminate")

0 commit comments

Comments
 (0)