As part of a project to support a PandABox in Bluesky, we are writing a pythonSoftIOC to allow control of all parameters. PandA provides types, values and metadata for each of these parameters, most of which map to existing record types, with the following exceptions:
- Enums with 128 entries and 30 character strings
- Table columns with units, precision and limits
- Table columns which are Enums
- Descriptions on everything
This repo contains some QSRV and P4P experiments that prototype extensions to normative types that allow these features to be expressed over PVA.
To run any of the examples below, do the following:
$ git clone https://github.com/thomascobb/nt_table_test.git $ cd nt_table_test $ pipenv install
This was the first requirement that pushed us from CA to PVA. This can be done at the moment with QSRV, although it needs epics-base/pva2pva#47 to allow typeids to be added to custom structures.
To allow these, I propose that there be a display_t
per column to
hold this information. As QSRV already produces display_t
with
different type for limitLow
and limitHigh
based on the value type,
I propose this be a structure like value
NTTable := structure string[] labels // The field names of each field in value structure value {scalar_t[] colname}0+ structure display {display_t[] colname}0+ string descriptor : opt alarm_t alarm : opt time_t timeStamp : opt
The above addition to NTTable doesn't support enums, because the display_t
doesn't contain the enum choices, instead embedding them into enum_t
. This
makes enum_t[]
the wrong choice for a table column as it allows each enum to
have a different choices array. I propose that instead an enum column is of type
uint
(or ushort
etc.) with the enum labels appearing in the
display_t
. I also propose that some fields of display_t
are made optional
so they do not need to be appear for scalar types that do not support them
display_t := structure string description // The following only for enums string[] enumLabels :opt // The following only for numeric scalar_t limitLow :opt scalar_t limitHigh :opt string units :opt int precision :opt enum_t form(3) :opt int index string[] choices ["Default", "String", "Binary", "Decimal", "Hex", "Exponential", "Engineering"]
The display_t
has a description
, but there are a number of references to
descriptor
in the normative types spec. I don't know the difference, but it
makes sense to me that description
belongs in display
. This is currently
missing from NTEnum
, so I propose that we instead use an NTScalar<uint>
instead with the above display_t
addition of enumLabels
.
I note that the NTTable
is missing a description
, this could be added at
the root of the structure, or an extra level added into the NTTable
display
structure, although if I was doing that I'd move the labels there:
NTTable := structure structure value {scalar_t[] colname}0+ structure display string description string[] labels // The field names of each field in value structure columns {display_t[] colname}0+ string descriptor : opt alarm_t alarm : opt time_t timeStamp : opt
This is probably too much of a breaking change however.
Running:
$ pipenv run python ./nt_table_ioc.py
The original enum gives pvget
output:
$ pvget -vv QSRV:OLD:ENUM QSRV:OLD:ENUM epics:nt/NTEnum:1.0 enum_t value (1) ONE int index 1 string[] choices ["ZERO", "ONE", "MANY"] alarm_t alarm int severity 0 int status 0 string message NO_ALARM time_t timeStamp 2021-11-12 15:24:03.841 long secondsPastEpoch 1636730643 int nanoseconds 841079061 int userTag 0
The new enum (with QSRV record
field removed, and remaining fields reordered):
$ pvget -vv QSRV:NEW:ENUM QSRV:NEW:ENUM epics:nt/NTScalar:1.0 int value 1 alarm_t alarm int severity 0 int status 0 string message NO_ALARM time_t timeStamp 2021-11-12 15:23:54.313 long secondsPastEpoch 1636730634 int nanoseconds 312931537 int userTag 0 structure display string description New Enum string[] enumLabels ["ZERO", "ONE", "MANY"]
Running:
$ pipenv run python ./nt_table_p4p.py
You get functionally equivalent output for pvget -vv P4P:OLD:ENUM
and
pvget -vv P4P:NEW:ENUM
.
Running:
$ pipenv run python ./nt_table_ioc.py
The original table gives pvget
output (with record
field removed, and
remaining fields reordered):
$ pvget -vv QSRV:TABLE QSRV:TABLE epics:nt/NTTable:1.0 string[] labels ["Enum", "Check Box", "String", "Float 64"] structure value int[] c1 [0,1,2] int[] c2 [1,0,1] string[] c3 ["a", "b", "c"] double[] c4 [38.5,37.5,36.5] alarm_t alarm int severity 0 int status 0 string message NO_ALARM time_t timeStamp 2021-11-12 15:32:40.907 long secondsPastEpoch 1636731160 int nanoseconds 907351844 int userTag 0
Running:
$ pipenv run python ./nt_table_p4p.py
You get functionally equivalent output for pvget -vv P4P:OLD:TABLE
, and for the
new table we get:
$ pvget -vv P4P:NEW:TABLE P4P:NEW:TABLE epics:nt/NTTable:1.0 string[] labels ["Enum", "Check Box", "String", "Float 64"] structure value uint[] enum [0,1,2] boolean[] checkBox [false,true,false] string[] string ["a", "b", "c"] double[] float64 [77.5,76.5,75.5] string descriptor alarm_t alarm int severity 0 int status 0 string message time_t timeStamp 2021-11-12 15:35:49.033 long secondsPastEpoch 1636731349 int nanoseconds 33052444 int userTag 0 structure display display_t enum string description An enum column string[] enumLabels ["ZERO", "ONE", "MANY"] display_t checkBox string description A checkBox column display_t string string description A string column display_t float64 string description A float64 column double limitLow 0 double limitHigh 10000.5 string units m int precision 1 enum_t form (0) Default int index 0 string[] choices ["Default", "String", "Binary", "Decimal", "Hex", "Exponential", "Engineering"]
At the moment, metadata can be added to columns of the table widget to allow enums and checkboxes. With the above changes it would be possible to use the metadata from the NTTable to populate these. I believe that the table widget currently only accepts strings so I have to convert to these in an embedded script:
from org.csstudio.display.builder.runtime.script import PVUtil, ScriptUtil
mode = PVUtil.getLong(pvs[0])
table = PVUtil.getTable(pvs[1])
if mode == 0: # Displaying
out = []
column_props = widget.getPropertyValue("columns")
for row in table:
new_row = []
for col, prop in zip(row, column_props):
if not isinstance(col, (str, unicode)):
options = prop.options()
if options.size() > 0:
col = options.getElement(col).value
else:
col = str(col)
new_row.append(col)
out.append(new_row)
widget.setValue(out)
widget.setPropertyValue("editable", False)
elif mode == 1: # Editing
widget.setPropertyValue("editable", True)
elif mode == 2: # Submit
pvs[0].write(0)
# This doesn't currently work...
pvs[1].write(widget.getValue())
When this is done we can see our NTTable (old or new):
This is discussed in more detail here: ControlSystemStudio/phoebus#1214