diff --git a/src/fastcs/demo/schema.json b/src/fastcs/demo/schema.json index 662e3862..9b2b09b2 100644 --- a/src/fastcs/demo/schema.json +++ b/src/fastcs/demo/schema.json @@ -46,6 +46,18 @@ }, { "$ref": "#/$defs/EnumMapping" + }, + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/$defs/EnumMapping" + }, + "type": "array" } ] }, diff --git a/src/fastcs/transports/epics/ca/ioc.py b/src/fastcs/transports/epics/ca/ioc.py index a70301c4..6dcdad45 100644 --- a/src/fastcs/transports/epics/ca/ioc.py +++ b/src/fastcs/transports/epics/ca/ioc.py @@ -35,11 +35,19 @@ class EpicsCAIOC: def __init__( self, controller_apis: list[ControllerAPI], - aliases: Mapping[str, str | EnumMapping], + aliases: Mapping[str, str | EnumMapping | list[str] | list[EnumMapping]], ): - alias_pvs = [ - value if isinstance(value, str) else value.pv for value in aliases.values() - ] + alias_pvs = [] + + for value in aliases.values(): + if isinstance(value, str): + alias_pvs.append(value) + elif isinstance(value, EnumMapping): + alias_pvs.append(value.pv) + else: + alias_pvs.extend( + item if isinstance(item, str) else item.pv for item in value + ) if duplicate_aliases := [ alias for alias, count in Counter(alias_pvs).items() if count > 1 @@ -130,7 +138,8 @@ def _add_sub_controller_pvi_info(parent: ControllerAPI): def _create_and_link_attribute_pvs( - root_controller_api: ControllerAPI, aliases: Mapping[str, str | EnumMapping] + root_controller_api: ControllerAPI, + aliases: Mapping[str, str | EnumMapping | list[str] | list[EnumMapping]], ) -> None: for controller_api in root_controller_api.walk_api(): pv_prefix = pv_prefix_from_path(controller_api.path) @@ -195,7 +204,7 @@ def _create_and_link_read_pv( pv_prefix: str, pv_name: str, attr_name: str, - alias: str | EnumMapping | None, + alias: str | EnumMapping | list[str] | list[EnumMapping] | None, attribute: AttrR[DType_T], ) -> None: pv = f"{pv_prefix}:{pv_name}" @@ -209,11 +218,14 @@ async def async_record_set(value: DType_T): record = _make_in_record(pv, attribute) - if isinstance(alias, str): - _add_alias(record, alias, attr_name) - elif isinstance(alias, EnumMapping): - enum_attr = _get_read_enum_attr_from_type(alias) - _add_read_enum_alias(alias, attribute, enum_attr) + aliases = alias if isinstance(alias, list) else [alias] + + for alias in aliases: + if isinstance(alias, str): + _add_alias(record, alias, attr_name) + elif isinstance(alias, EnumMapping): + enum_attr = _get_read_enum_attr_from_type(alias) + _add_read_enum_alias(alias, attribute, enum_attr) _add_attr_pvi_info(record, pv_prefix, attr_name, "r") attribute.add_on_update_callback(async_record_set) @@ -234,7 +246,7 @@ def _create_and_link_write_pv( pv_prefix: str, pv_name: str, attr_name: str, - alias: str | EnumMapping | None, + alias: str | EnumMapping | list[str] | list[EnumMapping] | None, attribute: AttrW[DType_T], ): pv = f"{pv_prefix}:{pv_name}" @@ -247,18 +259,22 @@ async def on_update(value): record = _make_out_record(pv, attribute, on_update=on_update) - if isinstance(alias, str): - _add_alias(record, alias, attr_name) - elif isinstance(alias, EnumMapping): - enum_attr = _get_write_enum_attr_from_type(alias) - _add_write_enum_alias(alias, attribute, enum_attr) + aliases = alias if isinstance(alias, list) else [alias] + + for alias in aliases: + if isinstance(alias, str): + _add_alias(record, alias, attr_name) + elif isinstance(alias, EnumMapping): + enum_attr = _get_write_enum_attr_from_type(alias) + _add_write_enum_alias(alias, attribute, enum_attr) _add_attr_pvi_info(record, pv_prefix, attr_name, "w") _sync_setpoint(pv, attribute, record) def _create_and_link_command_pvs( - root_controller_api: ControllerAPI, aliases: Mapping[str, str | EnumMapping] + root_controller_api: ControllerAPI, + aliases: Mapping[str, str | EnumMapping | list[str] | list[EnumMapping]], ) -> None: for controller_api in root_controller_api.walk_api(): pv_prefix = pv_prefix_from_path(controller_api.path) @@ -283,7 +299,7 @@ def _create_and_link_command_pv( pv_prefix: str, pv_name: str, attr_name: str, - alias: str | EnumMapping | None, + alias: str | EnumMapping | list[str] | list[EnumMapping] | None, method: Command, ) -> None: pv = f"{pv_prefix}:{pv_name}" @@ -301,11 +317,14 @@ async def wrapped_method(_: Any): ONAM="Active", ) - if isinstance(alias, str): - _add_alias(record, alias, attr_name) - elif isinstance(alias, EnumMapping): - enum_attr = _get_write_enum_attr_from_type(alias) - _add_command_enum_alias(alias, method, enum_attr) + aliases = alias if isinstance(alias, list) else [alias] + + for alias in aliases: + if isinstance(alias, str): + _add_alias(record, alias, attr_name) + elif isinstance(alias, EnumMapping): + enum_attr = _get_write_enum_attr_from_type(alias) + _add_command_enum_alias(alias, method, enum_attr) _add_attr_pvi_info(record, pv_prefix, attr_name, "x") diff --git a/src/fastcs/transports/epics/options.py b/src/fastcs/transports/epics/options.py index 1dadfe37..11558307 100644 --- a/src/fastcs/transports/epics/options.py +++ b/src/fastcs/transports/epics/options.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from dataclasses import dataclass, field from enum import Enum from pathlib import Path @@ -51,7 +52,9 @@ class EpicsCAOptions: __pydantic_config__: ClassVar[ConfigDict] = ConfigDict(extra="forbid") - aliases: dict[str, str | EnumMapping] = field(default_factory=dict) + aliases: Mapping[str, str | EnumMapping | list[str] | list[EnumMapping]] = field( + default_factory=dict + ) """Mapping of fastcs PV names to their aliases. Setpoint and readback PVs must be aliased separately. diff --git a/tests/data/schema.json b/tests/data/schema.json index b26fb2a9..0eeea1e4 100644 --- a/tests/data/schema.json +++ b/tests/data/schema.json @@ -46,6 +46,18 @@ }, { "$ref": "#/$defs/EnumMapping" + }, + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/$defs/EnumMapping" + }, + "type": "array" } ] }, diff --git a/tests/example_softioc.py b/tests/example_softioc.py index 57a94975..71608fca 100644 --- a/tests/example_softioc.py +++ b/tests/example_softioc.py @@ -56,7 +56,7 @@ def run(id="SOFTIOC_TEST_DEVICE"): EpicsCATransport( epicsca=EpicsCAOptions( aliases={ - f"{id}:B": f"{id}:AliasB", + f"{id}:B": [f"{id}:AliasB1", f"{id}:AliasB2"], f"{id}:B_RBV": f"{id}:AliasB_RBV", f"{id}:ChildVector:0:E": EnumMapping( pv=f"{id}:EnumAliasE", mapping={"Off": 1, "On": 2} diff --git a/tests/transports/epics/ca/test_softioc_system.py b/tests/transports/epics/ca/test_softioc_system.py index 8f4a3b5c..e50e8080 100644 --- a/tests/transports/epics/ca/test_softioc_system.py +++ b/tests/transports/epics/ca/test_softioc_system.py @@ -51,12 +51,15 @@ def test_ioc(softioc_subprocess: tuple[str, Queue]): } # Assert alias. Aliases do not show up in PVI structure - assert ctxt.get(f"{pv_prefix}:B") == ctxt.get(f"{pv_prefix}:AliasB") == 0 + assert ctxt.get(f"{pv_prefix}:B") == ctxt.get(f"{pv_prefix}:AliasB1") == 0 ctxt.put(f"{pv_prefix}:B", 10, wait=True) - assert ctxt.get(f"{pv_prefix}:AliasB") == 10 - ctxt.put(f"{pv_prefix}:AliasB", 20, wait=True) + assert ctxt.get(f"{pv_prefix}:AliasB1") == 10 + assert ctxt.get(f"{pv_prefix}:AliasB2") == 10 + ctxt.put(f"{pv_prefix}:AliasB1", 20, wait=True) assert ctxt.get(f"{pv_prefix}:B") == 20 - assert ctxt.get(f"{pv_prefix}:B_RBV") == ctxt.get(f"{pv_prefix}:AliasB_RBV") == 20 + ctxt.put(f"{pv_prefix}:AliasB2", 30, wait=True) + assert ctxt.get(f"{pv_prefix}:B") == 30 + assert ctxt.get(f"{pv_prefix}:B_RBV") == ctxt.get(f"{pv_prefix}:AliasB_RBV") == 30 # Assert enum alias. Enum aliases do not show up in PVI structure enum_value = ctxt.get(f"{pv_prefix}:ChildVector:0:E")