Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/fastcs/demo/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"$defs": {
"EpicsCAOptions": {
"additionalProperties": false,
"properties": {},
"properties": {
"aliases": {
"additionalProperties": {
"type": "string"
},
"title": "Aliases",
"type": "object"
}
},
"title": "EpicsCAOptions",
"type": "object"
},
Expand Down
44 changes: 35 additions & 9 deletions src/fastcs/transports/epics/ca/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
class EpicsCAIOC:
"""A softioc which handles one or more controllers."""

def __init__(self, controller_apis: list[ControllerAPI]):
def __init__(self, controller_apis: list[ControllerAPI], aliases: dict[str, str]):
self._controller_apis = controller_apis
for controller_api in controller_apis:
root_pv_prefix = pv_prefix_from_path(controller_api.path)
_add_pvi_info(f"{root_pv_prefix}:PVI")
_add_sub_controller_pvi_info(controller_api)

_create_and_link_attribute_pvs(controller_api)
_create_and_link_attribute_pvs(controller_api, aliases)
_create_and_link_command_pvs(controller_api)

def run(
Expand Down Expand Up @@ -107,7 +107,9 @@ def _add_sub_controller_pvi_info(parent: ControllerAPI):
_add_sub_controller_pvi_info(child)


def _create_and_link_attribute_pvs(root_controller_api: ControllerAPI) -> None:
def _create_and_link_attribute_pvs(
root_controller_api: ControllerAPI, aliases: dict[str, str]
) -> None:
for controller_api in root_controller_api.walk_api():
pv_prefix = pv_prefix_from_path(controller_api.path)

Expand All @@ -133,6 +135,7 @@ def _create_and_link_attribute_pvs(root_controller_api: ControllerAPI) -> None:
)
continue

alias = aliases.get(f"{pv_prefix}:{pv_name}", None)
match attribute:
case AttrRW():
if full_pv_name_length > (EPICS_MAX_NAME_LENGTH - 4):
Expand All @@ -144,19 +147,31 @@ def _create_and_link_attribute_pvs(root_controller_api: ControllerAPI) -> None:
attribute.enabled = False
else:
_create_and_link_read_pv(
pv_prefix, f"{pv_name}_RBV", attr_name, attribute
pv_prefix, f"{pv_name}_RBV", attr_name, alias, attribute
)
_create_and_link_write_pv(
pv_prefix, pv_name, attr_name, attribute
pv_prefix, pv_name, attr_name, alias, attribute
)
case AttrR():
_create_and_link_read_pv(pv_prefix, pv_name, attr_name, attribute)
_create_and_link_read_pv(
pv_prefix,
pv_name,
attr_name,
alias,
attribute,
)
case AttrW():
_create_and_link_write_pv(pv_prefix, pv_name, attr_name, attribute)
_create_and_link_write_pv(
pv_prefix, pv_name, attr_name, alias, attribute
)


def _create_and_link_read_pv(
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrR[DType_T]
pv_prefix: str,
pv_name: str,
attr_name: str,
alias: str | None,
attribute: AttrR[DType_T],
) -> None:
pv = f"{pv_prefix}:{pv_name}"

Expand All @@ -168,13 +183,22 @@ async def async_record_set(value: DType_T):
record.set(cast_to_epics_type(attribute.datatype, value))

record = _make_in_record(pv, attribute)

if alias:
suffix = "_RBV" if pv_name.endswith("_RBV") else ""
record.add_alias(f"{alias}{suffix}")

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

attribute.add_on_update_callback(async_record_set)


def _create_and_link_write_pv(
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[DType_T]
pv_prefix: str,
pv_name: str,
attr_name: str,
alias: str | None,
attribute: AttrW[DType_T],
) -> None:
pv = f"{pv_prefix}:{pv_name}"

Expand All @@ -191,6 +215,8 @@ async def set_setpoint_without_process(value: DType_T):
record.set(cast_to_epics_type(attribute.datatype, value), process=False)

record = _make_out_record(pv, attribute, on_update=on_update)
if alias:
record.add_alias(alias)

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

Expand Down
2 changes: 1 addition & 1 deletion src/fastcs/transports/epics/ca/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def connect(
self._controller_apis = controller_apis
self._loop = loop
self._pv_prefixes = [pv_prefix_from_path(api.path) for api in controller_apis]
self._ioc = EpicsCAIOC(controller_apis)
self._ioc = EpicsCAIOC(controller_apis, self.epicsca.aliases)

if self.docs is not None:
emit_docs_files(controller_apis, self.docs)
Expand Down
4 changes: 3 additions & 1 deletion src/fastcs/transports/epics/options.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import ClassVar
Expand Down Expand Up @@ -41,6 +41,8 @@ class EpicsCAOptions:

__pydantic_config__: ClassVar[ConfigDict] = ConfigDict(extra="forbid")

aliases: dict[str, str] = field(default_factory=dict)


@dataclass
class EpicsPVAOptions:
Expand Down
10 changes: 9 additions & 1 deletion tests/data/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"$defs": {
"EpicsCAOptions": {
"additionalProperties": false,
"properties": {},
"properties": {
"aliases": {
"additionalProperties": {
"type": "string"
},
"title": "Aliases",
"type": "object"
}
},
"title": "EpicsCAOptions",
"type": "object"
},
Expand Down
38 changes: 32 additions & 6 deletions tests/transports/epics/ca/test_softioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def test_create_and_link_read_pv(mocker: MockerFixture):
attribute = AttrR(Int())
attribute.add_on_update_callback = mocker.MagicMock()

_create_and_link_read_pv("PREFIX", "PV", "attr", attribute)
_create_and_link_read_pv("PREFIX", "PV", "attr", None, attribute)

make_record.assert_called_once_with("PREFIX:PV", attribute)
add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "r")
Expand All @@ -66,6 +66,32 @@ async def test_create_and_link_read_pv(mocker: MockerFixture):
record.set.assert_called_once_with(1)


@pytest.mark.asyncio
async def test_create_and_link_write_pv_adds_alias(mocker: MockerFixture):
make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_out_record")
record = make_record.return_value
record.add_alias = mocker.MagicMock()
attribute = mocker.MagicMock()

_create_and_link_write_pv("PREFIX", "PV", "attr", "alias", attribute)

make_record.assert_called_once_with("PREFIX:PV", attribute, on_update=mocker.ANY)
record.add_alias.assert_called_once_with("alias")


@pytest.mark.asyncio
async def test_create_and_link_read_pv_adds_alias_with_rbv(mocker: MockerFixture):
make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_in_record")
record = make_record.return_value
record.add_alias = mocker.MagicMock()
attribute = mocker.MagicMock()

_create_and_link_read_pv("PREFIX", "PV_RBV", "attr", "alias", attribute)

make_record.assert_called_once_with("PREFIX:PV_RBV", attribute)
record.add_alias.assert_called_once_with("alias_RBV")


@pytest.mark.parametrize(
"attribute,record_type,kwargs",
(
Expand Down Expand Up @@ -150,7 +176,7 @@ async def test_create_and_link_write_pv(mocker: MockerFixture):
attribute.put = mocker.AsyncMock()
attribute.add_sync_setpoint_callback = mocker.MagicMock()

_create_and_link_write_pv("PREFIX", "PV", "attr", attribute)
_create_and_link_write_pv("PREFIX", "PV", "attr", None, attribute)

make_record.assert_called_once_with("PREFIX:PV", attribute, on_update=mocker.ANY)
add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "w")
Expand Down Expand Up @@ -284,7 +310,7 @@ def test_ioc(mocker: MockerFixture, epics_controller_api: ControllerAPI):
"fastcs.transports.epics.ca.ioc._add_sub_controller_pvi_info"
)

EpicsCAIOC([epics_controller_api])
EpicsCAIOC([epics_controller_api], {})

# Check records are created
util_builder.boolIn.assert_called_once_with(
Expand Down Expand Up @@ -510,7 +536,7 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
long_rw_name = "attr_rw_with_a_reallyreally_long_name_that_is_too_long_for_RBV"
assert long_name_controller_api.attributes["attr_rw_short_name"].enabled
assert long_name_controller_api.attributes[long_attr_name].enabled
EpicsCAIOC([long_name_controller_api])
EpicsCAIOC([long_name_controller_api], {})
assert long_name_controller_api.attributes["attr_rw_short_name"].enabled
assert not long_name_controller_api.attributes[long_attr_name].enabled

Expand Down Expand Up @@ -599,10 +625,10 @@ def test_non_1d_waveforms_discarded(mocker: MockerFixture):
create_mock = mocker.patch(
"fastcs.transports.epics.ca.ioc._create_and_link_read_pv"
)
EpicsCAIOC([api])
EpicsCAIOC([api], {})

create_mock.assert_called_once_with(
DEVICE, "Waveform1d", "waveform_1d", api.attributes["waveform_1d"]
DEVICE, "Waveform1d", "waveform_1d", None, api.attributes["waveform_1d"]
)


Expand Down
Loading