Skip to content

Commit

Permalink
Add info tags to define PVI structure from controllers
Browse files Browse the repository at this point in the history
Refactor helper functions to preserve PV prefix until later so that PVI
PV can use it.
Remove call to SetDeviceName and construct full PVs when creating
records.
Add call to create info tags on all records as they are created to add
them to the PVI of the controller.
Add call to add sub controller PVI to parent PVI
  • Loading branch information
GDYendell committed Aug 21, 2024
1 parent 4370011 commit e46b028
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 52 deletions.
150 changes: 115 additions & 35 deletions src/fastcs/backends/epics/ioc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from collections.abc import Callable
from dataclasses import dataclass
from types import MethodType
from typing import Any
from typing import Any, Literal

from softioc import builder, softioc
from softioc.asyncio_dispatcher import AsyncioDispatcher
from softioc.pythonSoftIoc import RecordWrapper

from fastcs.attributes import AttrR, AttrRW, AttrW
from fastcs.controller import BaseController
from fastcs.datatypes import Bool, DataType, Float, Int, String
from fastcs.exceptions import FastCSException
from fastcs.mapping import Mapping
Expand All @@ -20,10 +21,10 @@ class EpicsIOCOptions:

class EpicsIOC:
def __init__(self, pv_prefix: str, mapping: Mapping):
builder.SetDeviceName(pv_prefix)
_create_and_link_attribute_pvs(pv_prefix, mapping)
_create_and_link_command_pvs(pv_prefix, mapping)

_create_and_link_attribute_pvs(mapping)
_create_and_link_command_pvs(mapping)
_add_pvi_refs(pv_prefix, mapping.controller)

def run(
self,
Expand All @@ -40,95 +41,174 @@ def run(
softioc.interactive_ioc(context)


def _create_and_link_attribute_pvs(mapping: Mapping) -> None:
def _create_and_link_attribute_pvs(pv_prefix: str, mapping: Mapping) -> None:
for single_mapping in mapping.get_controller_mappings():
path = single_mapping.controller.path
for attr_name, attribute in single_mapping.attributes.items():
attr_name = attr_name.title().replace("_", "")
pv_name = f"{':'.join(path)}:{attr_name}" if path else attr_name
pv_name = attr_name.title().replace("_", "")
_pv_prefix = ":".join([pv_prefix] + path)

match attribute:
case AttrRW():
_create_and_link_read_pv(pv_name + "_RBV", attribute)
_create_and_link_write_pv(pv_name, attribute)
_create_and_link_read_pv(
_pv_prefix, f"{pv_name}_RBV", attr_name, attribute
)
_create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute)
case AttrR():
_create_and_link_read_pv(pv_name, attribute)
_create_and_link_read_pv(_pv_prefix, pv_name, attr_name, attribute)
case AttrW():
_create_and_link_write_pv(pv_name, attribute)
_create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute)


def _create_and_link_read_pv(pv_name: str, attribute: AttrR) -> None:
record = _get_input_record(pv_name, attribute.datatype)
def _create_and_link_read_pv(
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrR
) -> None:
record = _get_input_record(f"{pv_prefix}:{pv_name}", attribute.datatype)

_add_pvi_info(record, pv_prefix, attr_name, "r")

async def async_wrapper(v):
record.set(v)

attribute.set_update_callback(async_wrapper)


def _get_input_record(pv_name: str, datatype: DataType) -> RecordWrapper:
def _get_input_record(pv: str, datatype: DataType) -> RecordWrapper:
match datatype:
case Bool(znam, onam):
return builder.boolIn(pv_name, ZNAM=znam, ONAM=onam)
return builder.boolIn(pv, ZNAM=znam, ONAM=onam)
case Int():
return builder.longIn(pv_name)
return builder.longIn(pv)
case Float(prec):
return builder.aIn(pv_name, PREC=prec)
return builder.aIn(pv, PREC=prec)
case String():
return builder.longStringIn(pv_name)
return builder.longStringIn(pv)
case _:
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")


def _create_and_link_write_pv(pv_name: str, attribute: AttrW) -> None:
def _create_and_link_write_pv(
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW
) -> None:
record = _get_output_record(
pv_name, attribute.datatype, on_update=attribute.process_without_display_update
f"{pv_prefix}:{pv_name}",
attribute.datatype,
on_update=attribute.process_without_display_update,
)

_add_pvi_info(record, pv_prefix, attr_name, "w")

async def async_wrapper(v):
record.set(v, process=False)

attribute.set_write_display_callback(async_wrapper)


def _get_output_record(pv_name: str, datatype: DataType, on_update: Callable) -> Any:
def _get_output_record(pv: str, datatype: DataType, on_update: Callable) -> Any:
match datatype:
case Bool(znam, onam):
return builder.boolOut(
pv_name,
pv,
ZNAM=znam,
ONAM=onam,
always_update=True,
on_update=on_update,
)
case Int():
return builder.longOut(pv_name, always_update=True, on_update=on_update)
return builder.longOut(pv, always_update=True, on_update=on_update)
case Float(prec):
return builder.aOut(
pv_name, always_update=True, on_update=on_update, PREC=prec
)
return builder.aOut(pv, always_update=True, on_update=on_update, PREC=prec)
case String():
return builder.longStringOut(
pv_name, always_update=True, on_update=on_update
)
return builder.longStringOut(pv, always_update=True, on_update=on_update)
case _:
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")


def _create_and_link_command_pvs(mapping: Mapping) -> None:
def _create_and_link_command_pvs(pv_prefix: str, mapping: Mapping) -> None:
for single_mapping in mapping.get_controller_mappings():
path = single_mapping.controller.path
for attr_name, method in single_mapping.command_methods.items():
attr_name = attr_name.title().replace("_", "")
pv_name = f"{':'.join(path)}:{attr_name}" if path else attr_name
pv_name = attr_name.title().replace("_", "")
_pv_prefix = ":".join([pv_prefix] + path)

_create_and_link_command_pv(
pv_name, MethodType(method.fn, single_mapping.controller)
_pv_prefix,
pv_name,
attr_name,
MethodType(method.fn, single_mapping.controller),
)


def _create_and_link_command_pv(pv_name: str, method: Callable) -> None:
def _create_and_link_command_pv(
pv_prefix: str, pv_name: str, attr_name: str, method: Callable
) -> None:
async def wrapped_method(_: Any):
await method()

builder.aOut(pv_name, initial_value=0, always_update=True, on_update=wrapped_method)
record = builder.aOut(
f"{pv_prefix}:{pv_name}",
initial_value=0,
always_update=True,
on_update=wrapped_method,
)

_add_pvi_info(record, pv_prefix, attr_name, "x")


def _add_pvi_info(
record: RecordWrapper,
prefix: str,
name: str,
access_mode: Literal["r", "w", "rw", "x"],
):
"""Add an info tag to a record to include it in the PVI for the controller.
Args:
record: Record to add info tag to
prefix: PV prefix of controller
name: Name of parameter to add to PVI
access_mode: Access mode of parameter
"""
record.add_info(
"Q:group",
{
f"{prefix}:PVI": {
f"pvi.{name}.{access_mode}": {
"+channel": "NAME",
"+type": "plain",
"+trigger": f"pvi.{name}.{access_mode}",
}
}
},
)


def _add_pvi_refs(pv_prefix: str, parent: BaseController):
"""Add PVI references from controller to its sub controllers, recursively.
Args:
pv_prefix: PV Prefix of IOC
parent: Controller to add PVI refs for
"""
parent_pvi = ":".join([pv_prefix] + parent.path + ["PVI"])

for child in parent.get_sub_controllers().values():
child_pvi = ":".join([pv_prefix] + child.path + ["PVI"])
child_name = f"pvi.{child.path[-1].lower()}.d"

# Make a record with an info tag to add child to parent PVI structure
dummy_record = builder.longStringIn(f"{child_pvi}_PV", initial_value=child_pvi)
dummy_record.add_info(
"Q:group",
{
parent_pvi: {
child_name: {
"+channel": "VAL",
"+type": "plain",
"+trigger": child_name,
}
}
},
)

_add_pvi_refs(pv_prefix, child)
107 changes: 90 additions & 17 deletions tests/backends/epics/test_ioc.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,104 @@
from pytest_mock import MockerFixture

from fastcs.backends.epics.ioc import EpicsIOC
from fastcs.backends.epics.ioc import EpicsIOC, _add_pvi_info, _add_pvi_refs
from fastcs.mapping import Mapping


def test_ioc(mocker: MockerFixture, mapping: Mapping):
builder_mock = mocker.patch("fastcs.backends.epics.ioc.builder")
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info")

EpicsIOC("DEVICE", mapping)

builder_mock.aIn.assert_called_once_with("ReadWriteFloat_RBV", PREC=2)
builder_mock.aOut.assert_any_call(
"ReadWriteFloat", always_update=True, on_update=mocker.ANY, PREC=2
# Check records are created
builder.aIn.assert_called_once_with("DEVICE:ReadWriteFloat_RBV", PREC=2)
builder.aOut.assert_any_call(
"DEVICE:ReadWriteFloat", always_update=True, on_update=mocker.ANY, PREC=2
)
builder_mock.boolIn.assert_called_once_with("ReadBool", ZNAM="OFF", ONAM="ON")
builder_mock.boolOut.assert_called_once_with(
"WriteBool", ZNAM="OFF", ONAM="ON", always_update=True, on_update=mocker.ANY
builder.boolIn.assert_called_once_with("DEVICE:ReadBool", ZNAM="OFF", ONAM="ON")
builder.boolOut.assert_called_once_with(
"DEVICE:WriteBool",
ZNAM="OFF",
ONAM="ON",
always_update=True,
on_update=mocker.ANY,
)
builder_mock.longIn.assert_any_call("ReadInt")
builder_mock.longIn.assert_any_call("ReadWriteInt_RBV")
builder_mock.longOut.assert_called_with(
"ReadWriteInt", always_update=True, on_update=mocker.ANY
builder.longIn.assert_any_call("DEVICE:ReadInt")
builder.longIn.assert_any_call("DEVICE:ReadWriteInt_RBV")
builder.longOut.assert_called_with(
"DEVICE:ReadWriteInt", always_update=True, on_update=mocker.ANY
)
builder_mock.longStringIn.assert_called_once_with("StringEnum_RBV")
builder_mock.longStringOut.assert_called_once_with(
"StringEnum", always_update=True, on_update=mocker.ANY
builder.longStringIn.assert_called_once_with("DEVICE:StringEnum_RBV")
builder.longStringOut.assert_called_once_with(
"DEVICE:StringEnum", always_update=True, on_update=mocker.ANY
)
builder_mock.aOut.assert_any_call(
"Go", initial_value=0, always_update=True, on_update=mocker.ANY
builder.aOut.assert_any_call(
"DEVICE:Go", initial_value=0, always_update=True, on_update=mocker.ANY
)

# Check info tags are added
add_pvi_info.assert_has_calls(
[
mocker.call(builder.boolIn.return_value, "DEVICE", "read_bool", "r"),
mocker.call(builder.longIn.return_value, "DEVICE", "read_int", "r"),
mocker.call(builder.aIn.return_value, "DEVICE", "read_write_float", "r"),
mocker.call(builder.aOut.return_value, "DEVICE", "read_write_float", "w"),
mocker.call(builder.longIn.return_value, "DEVICE", "read_write_int", "r"),
mocker.call(builder.longOut.return_value, "DEVICE", "read_write_int", "w"),
mocker.call(
builder.longStringIn.return_value, "DEVICE", "string_enum", "r"
),
mocker.call(
builder.longStringOut.return_value, "DEVICE", "string_enum", "w"
),
mocker.call(builder.boolOut.return_value, "DEVICE", "write_bool", "w"),
mocker.call(builder.aOut.return_value, "DEVICE", "go", "x"),
]
)


def test_add_pvi_info(mocker: MockerFixture):
record = mocker.MagicMock()

_add_pvi_info(record, "DEVICE", "attr", "r")

record.add_info.assert_called_once_with(
"Q:group",
{
"DEVICE:PVI": {
"pvi.attr.r": {
"+channel": "NAME",
"+type": "plain",
"+trigger": "pvi.attr.r",
}
}
},
)


def test_add_pvi_refs(mocker: MockerFixture):
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
controller = mocker.MagicMock()
controller.path = []
child = mocker.MagicMock()
child.path = ["Child"]
controller.get_sub_controllers.return_value = {"d": child}

_add_pvi_refs("DEVICE", controller)

builder.longStringIn.assert_called_once_with(
"DEVICE:Child:PVI_PV", initial_value="DEVICE:Child:PVI"
)
record = builder.longStringIn.return_value
record.add_info.assert_called_once_with(
"Q:group",
{
"DEVICE:PVI": {
"pvi.child.d": {
"+channel": "VAL",
"+type": "plain",
"+trigger": "pvi.child.d",
}
}
},
)

0 comments on commit e46b028

Please sign in to comment.