Input-Output Controllers (IOCs)¶
In this section, we will review some of the example IOCs that are included with caproto, intended as demonstrations of what is possible.
Note
Please note that this is preliminary API and may very likely change in future releases.
Type Annotations¶
Python type annotations are the primary method used to generate PVA-compatible data structures for usage in an IOC.
This makes for reasonably compact and easy definitions of new types. A simple example may be:
from caproto import pva
@pva.pva_dataclass
class MyData:
value: int
info: str
This creates a data structure named MyData
with two fields value
(containing an integer - int64
) and info
(containing a variable-length
string).
Arrays¶
Arrays can be specified using Python standard library annotations:
from typing import List
from caproto import pva
@pva.pva_dataclass
class MyData:
value_array: List[int]
info: str
Specific Types¶
Using the Python built-in types implies usage of the maximum supported
compatible data type - that is, int
implies a 64-bit signed integer,
float
implies a double (64-bit).
from typing import List
from caproto import pva
@pva.pva_dataclass
class MyData:
list_of_bytes: List[pva.UInt8]
list_of_int32: List[pva.Int32]
float_value: pva.Float32
Unions and Nested Types¶
Nesting data types is supported, inside or outside of the class body:
from caproto import pva
@pva.pva_dataclass
class NestedData:
@pva.pva_dataclass
class MyData:
value: int
info: str
my_data: MyData
value: float
Basic support for union types is also present:
from typing import Union
from caproto import pva
@pva.pva_dataclass
class MyData:
to_form_a: str
more_perfect: Union[float, str]
Alternatively:
from typing import Union
from caproto import pva
@pva.pva_dataclass(union=True)
class MyUnion:
string_value: str
float_value: float
@pva.pva_dataclass
class MyData:
to_form_a: str
more_perfect: MyUnion
The key for the above is that only one value may be non-None
for it to
be selected automatically.
Unsupported Types¶
Some types are not yet supported, though they currently have annotations:
BoundedString
Float16
Float128
PVAGroup¶
Note
While caproto-pva supports an “expert” (non-magical) method of writing
IOCs, but these are considered advanced usage and are not documented here.
An example of one may be found in the “advanced” example
(caproto.pva.ioc_examples.advanced
)
To define an IOC, we first define a new PVAGroup
subclass.
import caproto.pva as pva
class MyIOC(pva.PVAGroup):
...
There is no data contained in this just yet, so we need to add some
pvaproperty
attributes. These act something like
Python’s built-in property decorator.
The following IOC class, upon instantiation, will generate one PV named
"value"
- not including a prefix - which contains an NTScalar
floating
point value:
import caproto.pva as pva
class MyIOC(pva.PVAGroup):
value = pva.pvaproperty(value=1.234)
Data type support is not just limited to normative types. One may create their own data type as described in Type Annotations.
import caproto.pva as pva
@pva.pva_dataclass
class MyData:
value: int
info: str
class MyIOC(pva.PVAGroup):
value = pva.pvaproperty(value=MyData(value=1, info='testing'))
Adding a bit of boilerplate, any of the above IOCs can be run by simply executing the given Python source code:
import caproto.pva as pva
class MyIOC(pva.PVAGroup):
value = pva.pvaproperty(value=1.234)
def main():
ioc_options, run_options = ioc_arg_parser(
default_prefix='caproto:pva:',
desc='A basic caproto-pva test server.'
)
ioc = MyIOC(**ioc_options)
run(ioc.pvdb, **run_options)
if __name__ == '__main__':
main()
The ioc_arg_parser()
handles parsing arguments, and
run()
handles setting up the async library and booting the
IOC.
pvaproperty¶
Handling put¶
import caproto.pva as pva
class MyIOC(pva.PVAGroup):
value = pva.pvaproperty(value=1.234)
@value.putter
async def value(self, instance, update: pva.WriteUpdate):
"""
Put handler.
Default handling: `update.accept()` (take everything)
"""
Key-based accepting and rejecting of values:
update.accept('value', 'info')
update.reject('info')
Checking if a key is included in an update, and updating based on that:
if 'value' in update: instance.value = update.instance.value
Handling RPC calls¶
class MyIOC(PVAGroup):
rpc = pvaproperty(value=MyData())
@rpc.call
async def rpc(self, instance, data):
# It's likely but not required that normative type stuff comes
# through ``data``.
print('RPC call data is', data)
print('Scheme:', data.scheme)
print('Query:', data.query)
print('Path:', data.path)
# Echo back the query value, if available:
query = data.query
value = int(getattr(query, 'value', '1234'))
return MyData(value=value)
Startup and shutdown methods¶
When the server starts up, it can optionally call startup hooks for each pvaproperty.
class MyIOC(PVAGroup):
swap_a = pvaproperty(value=6)
swap_b = pvaproperty(value=7)
@swap_a.startup
async def swap_a(self, instance, async_lib):
while True:
async with self.swap_a as a, self.swap_b as b:
# Swap values
a.value, b.value = b.value, a.value
await async_lib.library.sleep(2.0)
async_lib
here is a shim that abstracts away the async library, whether
it be asyncio, curio, trio, or something else.
A shutdown hook has the same API and allows for resource cleanup prior to the process exiting.
Handling Authentication¶
This isn’t fully exposed yet in the pvaproperty API.
AsyncLibraryLayer¶
Frequently referred to as async_lib
in startup
hooks, the
AsyncLibraryLayer
class is a shim that abstracts away the async
library, whether it be asyncio, curio, trio, or something else.
Attributes guaranteed to exist are:
name
- the name of the asyncio librarylibrary
- the library module itself (e.g.,asyncio
)ThreadsafeQueue
- a thread-safe queue with a common API
Additionally, one may assume the library.sleep
coroutine exists.
Using the IOC Examples¶
They can be started like so:
$ python -m caproto.pva.ioc_examples.normative
[I 13:02:01.350 server: 127] Asyncio server starting up...
[I 13:02:01.350 server: 128] Server GUID is: 0x313565616438386234356637
[I 13:02:01.350 server: 143] Listening on 0.0.0.0:5075
[I 13:02:01.351 server: 204] Server startup complete.
and stopped using Ctrl+C:
^C
[I 13:02:19.129 server: 212] Server task cancelled. Will shut down.
[I 13:02:19.129 server: 222] Server exiting....
Use --list-pvs
to display which PVs they serve:
$ python -m caproto.pva.ioc_examples.normative --list-pvs
[I 13:02:33.318 server: 127] Asyncio server starting up...
[I 13:02:33.318 server: 128] Server GUID is: 0x393063653765653064633235
[I 13:02:33.318 server: 143] Listening on 0.0.0.0:5075
[I 13:02:33.319 server: 204] Server startup complete.
[I 13:02:33.319 server: 207] PVs available:
caproto:pva:bool
caproto:pva:int
caproto:pva:float
caproto:pva:str
caproto:pva:int_array
caproto:pva:float_array
caproto:pva:string_array
server
and use --prefix
to conveniently customize the PV prefix.
Type python3 -m caproto.pva.ioc_examples.normative -h
for more options.
caproto-pva
does not yet have an option for getting a PV list as the
built-in epics-base pvlist
does, but the servers support the interface
required for this information.
# NOTE: The following requires epics-base to be installed, for now.
$ pvlist
GUID 0x393063653765653064633235, version 1: tcp@[10.0.0.2:5075]
$ pvlist 0x393063653765653064633235
caproto:pva:bool
caproto:pva:float
caproto:pva:float_array
caproto:pva:int
caproto:pva:int_array
caproto:pva:str
caproto:pva:string_array
Examples¶
Normative Types¶
Normative types (NTScalar
, NTScalarArray
) are data types that are
of a standardized form, containing data one might expect from Channel Access
V3 data types. At minimum, these are guaranteed to contain a value
field,
but may also include control, display, and alarm information with limits,
units, and descriptions.
caproto server normative values (by default) include all additional metadata.
A basic caproto-pva test server with normative types. |
|
|
#!/usr/bin/env python3
"""
A basic caproto-pva test server with normative types.
"""
from caproto.pva.server import (PVAGroup, ServerRPC, ioc_arg_parser,
pvaproperty, run)
class NormativeIOC(PVAGroup):
nt_bool = pvaproperty(value=True)
nt_int = pvaproperty(value=42)
nt_float = pvaproperty(value=42.1)
nt_string = pvaproperty(value='test')
nt_int_array = pvaproperty(value=[42])
nt_float_array = pvaproperty(value=[42.1])
nt_string_array = pvaproperty(value=['test'])
def main():
ioc_options, run_options = ioc_arg_parser(
default_prefix='caproto:pva:',
desc='A basic caproto-pva test server for normative types.'
)
ioc = NormativeIOC(**ioc_options)
server_info = ServerRPC(prefix='', server_instance=ioc)
ioc.pvdb.update(server_info.pvdb)
run(ioc.pvdb, **run_options)
if __name__ == '__main__':
main()
Groups¶
The full set of possibilities with what groups and properties can do.
A basic caproto-pva test server using PVAGroup. |
|
|
#!/usr/bin/env python3
"""
A basic caproto-pva test server using PVAGroup.
"""
import caproto.pva as pva
from caproto.pva.server import (PVAGroup, ioc_arg_parser, pva_dataclass,
pvaproperty, run)
@pva_dataclass
class MyData:
value: int
info: str
@pva_dataclass
class NestedData:
my_data: MyData
value: float
class MyIOC(PVAGroup):
test = pvaproperty(value=MyData(value=5, info='a string'))
test2 = pvaproperty(value=MyData(value=6, info='a different string'))
test3 = pvaproperty(value=NestedData)
long_write = pvaproperty(value=MyData())
rpc = pvaproperty(value=MyData())
@test.putter # (accepts=['value']) (potential API?)
async def test(self, instance, update: pva.WriteUpdate):
"""
Put handler.
Default handling: `update.accept()` (take everything)
The following also work::
1. update.accept('value', 'info')
2. update.reject('info')
3. if 'value' in update:
instance.value = update.instance.value
"""
...
@test.startup
async def test(self, instance, async_lib):
self.async_lib = async_lib
while True:
async with self.test as test:
test.value = test.value + 1
test.info = f'testing {test.value}'
await async_lib.library.sleep(0.5)
@test.shutdown
async def test(self, instance, async_lib):
print('shutdown')
@test2.startup
async def test2(self, instance, async_lib):
while True:
async with self.test2 as test2, self.test3 as test3:
# Swap values
test2.value, test3.value = int(test3.value), float(test2.value)
await async_lib.library.sleep(2.0)
@rpc.call
async def rpc(self, instance, data):
# Some awf... nice normative type stuff comes through here (NTURI):
print('RPC call data is', data)
print('Scheme:', data.scheme)
print('Query:', data.query)
print('Path:', data.path)
# Echo back the query value, if available:
query = data.query
value = int(getattr(query, 'value', '1234'))
return MyData(value=value)
@long_write.putter
async def long_write(self, instance, update: pva.WriteUpdate):
await self.async_lib.library.sleep(10)
def main():
ioc_options, run_options = ioc_arg_parser(
default_prefix='caproto:pva:',
desc='A basic caproto-pva test server.'
)
ioc = MyIOC(**ioc_options)
server_info = pva.ServerRPC(prefix='', server_instance=ioc)
ioc.pvdb.update(server_info.pvdb)
run(ioc.pvdb, **run_options)
if __name__ == '__main__':
main()