diff --git a/docs/snippets/dynamic.py b/docs/snippets/dynamic.py index 9dad42e7a..a66d04b08 100644 --- a/docs/snippets/dynamic.py +++ b/docs/snippets/dynamic.py @@ -1,17 +1,17 @@ -from __future__ import annotations - import json -from dataclasses import dataclass -from typing import Any, Literal +from dataclasses import KW_ONLY, dataclass +from typing import Any, Literal, TypeVar from pydantic import BaseModel, ConfigDict, ValidationError -from fastcs.attributes import AttrHandlerRW, Attribute, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings from fastcs.controller import Controller from fastcs.datatypes import Bool, DataType, Float, Int, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsIOCOptions @@ -46,74 +46,95 @@ def create_attributes(parameters: dict[str, Any]) -> dict[str, Attribute]: print(f"Failed to validate parameter '{parameter}'\n{e}") continue - handler = TemperatureControllerHandler(parameter.command) + io_ref = TemperatureControllerAttributeIORef(parameter.command) match parameter.access_mode: case "r": - attributes[name] = AttrR(parameter.fastcs_datatype, handler=handler) + attributes[name] = AttrR(parameter.fastcs_datatype, io_ref=io_ref) case "rw": - attributes[name] = AttrRW(parameter.fastcs_datatype, handler=handler) + attributes[name] = AttrRW(parameter.fastcs_datatype, io_ref=io_ref) return attributes +NumberT = TypeVar("NumberT", int, float) + + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | None = None - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}?\r\n" - ) + +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection): + super().__init__() + + self._connection = connection + + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any): - await self.controller.connection.send_command( - f"{self.command_name}={value}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class TemperatureRampController(Controller): - def __init__(self, index: int, connection: IPConnection): - super().__init__(f"Ramp {index}") + def __init__( + self, + index: int, + parameters: dict[str, TemperatureControllerParameter], + io: TemperatureControllerAttributeIO, + ): + self._parameters = parameters + super().__init__(f"Ramp{index}", ios=[io]) - self.connection = connection - - async def initialise(self, parameters: dict[str, Any]): - self.attributes.update(create_attributes(parameters)) + async def initialise(self): + self.attributes.update(create_attributes(self._parameters)) class TemperatureController(Controller): def __init__(self, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + self._io = TemperatureControllerAttributeIO(self._connection) + super().__init__(ios=[self._io]) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) async def initialise(self): await self.connect() - api = json.loads((await self.connection.send_query("API?\r\n")).strip("\r\n")) + api = json.loads((await self._connection.send_query("API?\r\n")).strip("\r\n")) ramps_api = api.pop("Ramps") self.attributes.update(create_attributes(api)) for idx, ramp_parameters in enumerate(ramps_api): - ramp_controller = TemperatureRampController(idx + 1, self.connection) + ramp_controller = TemperatureRampController( + idx + 1, ramp_parameters, self._io + ) + await ramp_controller.initialise() self.register_sub_controller(f"Ramp{idx + 1:02d}", ramp_controller) - await ramp_controller.initialise(ramp_parameters) - await self.connection.close() + await self._connection.close() -epics_options = EpicsCAOptions(ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) +epics_ca = EpicsCATransport(ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(connection_settings), [epics_ca]) + -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static02.py b/docs/snippets/static02.py index 376777993..e0697dff3 100644 --- a/docs/snippets/static02.py +++ b/docs/snippets/static02.py @@ -7,4 +7,6 @@ class TemperatureController(Controller): fastcs = FastCS(TemperatureController(), []) -# fastcs.run() # Commented as this will block + +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static03.py b/docs/snippets/static03.py index 57d210c07..27b065840 100644 --- a/docs/snippets/static03.py +++ b/docs/snippets/static03.py @@ -9,4 +9,6 @@ class TemperatureController(Controller): fastcs = FastCS(TemperatureController(), []) -# fastcs.run() # Commented as this will block + +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static04.py b/docs/snippets/static04.py index 51fc522e8..27afbc345 100644 --- a/docs/snippets/static04.py +++ b/docs/snippets/static04.py @@ -2,7 +2,7 @@ from fastcs.controller import Controller from fastcs.datatypes import String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsIOCOptions @@ -10,7 +10,8 @@ class TemperatureController(Controller): device_id = AttrR(String()) -epics_options = EpicsCAOptions(ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) -fastcs = FastCS(TemperatureController(), [epics_options]) +epics_ca = EpicsCATransport(ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) +fastcs = FastCS(TemperatureController(), [epics_ca]) -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static05.py b/docs/snippets/static05.py index 30ddac1d5..6aba60529 100644 --- a/docs/snippets/static05.py +++ b/docs/snippets/static05.py @@ -4,7 +4,8 @@ from fastcs.controller import Controller from fastcs.datatypes import String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions, EpicsGUIOptions +from fastcs.transport.epics.ca.options import EpicsGUIOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsIOCOptions @@ -15,12 +16,10 @@ class TemperatureController(Controller): gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) -fastcs = FastCS(TemperatureController(), [epics_options]) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) +fastcs = FastCS(TemperatureController(), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static06.py b/docs/snippets/static06.py index c6be250c0..5f08ce1ec 100644 --- a/docs/snippets/static06.py +++ b/docs/snippets/static06.py @@ -5,7 +5,7 @@ from fastcs.controller import Controller from fastcs.datatypes import String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions @@ -16,22 +16,20 @@ def __init__(self, settings: IPConnectionSettings): super().__init__() self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static07.py b/docs/snippets/static07.py index 0d1ae3dfe..c8836b327 100644 --- a/docs/snippets/static07.py +++ b/docs/snippets/static07.py @@ -1,63 +1,59 @@ -from __future__ import annotations - from dataclasses import dataclass from pathlib import Path +from typing import TypeVar -from fastcs.attributes import AttrHandlerR, AttrR +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions +NumberT = TypeVar("NumberT", int, float) + @dataclass -class IDUpdater(AttrHandlerR): +class IDAttributeIORef(AttributeIORef): update_period: float | None = 0.2 - _controller: TemperatureController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController) - self._controller = controller - @property - def controller(self) -> TemperatureController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class IDAttributeIO(AttributeIO[NumberT, IDAttributeIORef]): + def __init__(self, connection: IPConnection): + super().__init__() - return self._controller + self._connection = connection - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query("ID?\r\n") + async def update(self, attr: AttrR[NumberT, IDAttributeIORef]): + response = await self._connection.send_query("ID?\r\n") value = response.strip("\r\n") - await attr.set(value) + await attr.set(attr.dtype(value)) class TemperatureController(Controller): - device_id = AttrR(String(), handler=IDUpdater()) + device_id = AttrR(String(), io_ref=IDAttributeIORef()) def __init__(self, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[IDAttributeIO(self._connection)]) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static08.py b/docs/snippets/static08.py index cf45e1b29..fdc7afc0b 100644 --- a/docs/snippets/static08.py +++ b/docs/snippets/static08.py @@ -1,67 +1,65 @@ -from __future__ import annotations - -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path +from typing import TypeVar -from fastcs.attributes import AttrHandlerR, AttrR +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Float, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerUpdater(AttrHandlerR): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController) - self._controller = controller - @property - def controller(self) -> TemperatureController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection): + super().__init__() - return self._controller + self._connection = connection - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerUpdater("ID")) - power = AttrR(Float(), handler=TemperatureControllerUpdater("P")) + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) def __init__(self, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static09.py b/docs/snippets/static09.py index df072cc82..48936caf0 100644 --- a/docs/snippets/static09.py +++ b/docs/snippets/static09.py @@ -1,74 +1,72 @@ -from __future__ import annotations - -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import Any +from typing import TypeVar -from fastcs.attributes import AttrHandlerRW, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Float, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController) - self._controller = controller - @property - def controller(self) -> TemperatureController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection): + super().__init__() - return self._controller + self._connection = connection - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any): - await self.controller.connection.send_command( - f"{self.command_name}={attr.dtype(value)}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerHandler("ID")) - power = AttrR(Float(), handler=TemperatureControllerHandler("P")) - ramp_rate = AttrRW(Float(), handler=TemperatureControllerHandler("R")) + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) + ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef("R")) def __init__(self, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static10.py b/docs/snippets/static10.py index 44f569f6d..fcd24e2d6 100644 --- a/docs/snippets/static10.py +++ b/docs/snippets/static10.py @@ -1,94 +1,90 @@ -from __future__ import annotations - -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import Any +from typing import TypeVar -from fastcs.attributes import AttrHandlerRW, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Float, Int, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | TemperatureRampController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController | TemperatureRampController) - self._controller = controller - @property - def controller(self) -> TemperatureController | TemperatureRampController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection, suffix: str = ""): + super().__init__() - return self._controller + self._connection = connection + self._suffix = suffix - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}{self.controller.suffix}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}{self._suffix}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any): - await self.controller.connection.send_command( - f"{self.command_name}{self.controller.suffix}={attr.dtype(value)}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}{self._suffix}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class TemperatureRampController(Controller): - start = AttrRW(Int(), handler=TemperatureControllerHandler("S")) - end = AttrRW(Int(), handler=TemperatureControllerHandler("E")) - - def __init__(self, index: int, connection: IPConnection): - self.suffix = f"{index:02d}" - - super().__init__(f"Ramp{self.suffix}") + start = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="S")) + end = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="E")) - self.connection = connection + def __init__(self, index: int, connection: IPConnection) -> None: + suffix = f"{index:02d}" + super().__init__( + f"Ramp{suffix}", ios=[TemperatureControllerAttributeIO(connection, suffix)] + ) class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerHandler("ID")) - power = AttrR(Float(), handler=TemperatureControllerHandler("P")) - ramp_rate = AttrRW(Float(), handler=TemperatureControllerHandler("R")) - - suffix = "" + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) + ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef("R")) def __init__(self, ramp_count: int, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) self._ramp_controllers: list[TemperatureRampController] = [] - for idx in range(1, ramp_count + 1): - ramp_controller = TemperatureRampController(idx, self.connection) - self._ramp_controllers.append(ramp_controller) - self.register_sub_controller(f"R{idx}", ramp_controller) + for index in range(1, ramp_count + 1): + controller = TemperatureRampController(index, self._connection) + self._ramp_controllers.append(controller) + self.register_sub_controller(f"R{index}", controller) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(4, connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(4, connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static11.py b/docs/snippets/static11.py index 55d3ab29f..2387025d0 100644 --- a/docs/snippets/static11.py +++ b/docs/snippets/static11.py @@ -1,48 +1,49 @@ -from __future__ import annotations - import enum -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import Any +from typing import TypeVar -from fastcs.attributes import AttrHandlerRW, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Enum, Float, Int, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | TemperatureRampController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController | TemperatureRampController) - self._controller = controller - @property - def controller(self) -> TemperatureController | TemperatureRampController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection, suffix: str = ""): + super().__init__() - return self._controller + self._connection = connection + self._suffix = suffix - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}{self.controller.suffix}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}{self._suffix}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any): - await self.controller.connection.send_command( - f"{self.command_name}{self.controller.suffix}={attr.dtype(value)}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}{self._suffix}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class OnOffEnum(enum.StrEnum): @@ -51,51 +52,46 @@ class OnOffEnum(enum.StrEnum): class TemperatureRampController(Controller): - start = AttrRW(Int(), handler=TemperatureControllerHandler("S")) - end = AttrRW(Int(), handler=TemperatureControllerHandler("E")) - enabled = AttrRW(Enum(OnOffEnum), handler=TemperatureControllerHandler("N")) - - def __init__(self, index: int, connection: IPConnection): - self.suffix = f"{index:02d}" - - super().__init__(f"Ramp{self.suffix}") - - self.connection = connection + start = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="S")) + end = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="E")) + enabled = AttrRW(Enum(OnOffEnum), io_ref=TemperatureControllerAttributeIORef("N")) + + def __init__(self, index: int, connection: IPConnection) -> None: + suffix = f"{index:02d}" + super().__init__( + f"Ramp{suffix}", ios=[TemperatureControllerAttributeIO(connection, suffix)] + ) class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerHandler("ID")) - power = AttrR(Float(), handler=TemperatureControllerHandler("P")) - ramp_rate = AttrRW(Float(), handler=TemperatureControllerHandler("R")) - - suffix = "" + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) + ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef("R")) def __init__(self, ramp_count: int, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) self._ramp_controllers: list[TemperatureRampController] = [] - for idx in range(1, ramp_count + 1): - ramp_controller = TemperatureRampController(idx, self.connection) - self._ramp_controllers.append(ramp_controller) - self.register_sub_controller(f"R{idx}", ramp_controller) + for index in range(1, ramp_count + 1): + controller = TemperatureRampController(index, self._connection) + self._ramp_controllers.append(controller) + self.register_sub_controller(f"R{index}", controller) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(4, connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(4, connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static12.py b/docs/snippets/static12.py index 1613c1d7c..130e34cea 100644 --- a/docs/snippets/static12.py +++ b/docs/snippets/static12.py @@ -1,50 +1,51 @@ -from __future__ import annotations - import enum import json -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import Any +from typing import TypeVar -from fastcs.attributes import AttrHandlerRW, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Enum, Float, Int, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions from fastcs.wrappers import scan +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | TemperatureRampController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController | TemperatureRampController) - self._controller = controller - @property - def controller(self) -> TemperatureController | TemperatureRampController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection, suffix: str = ""): + super().__init__() - return self._controller + self._connection = connection + self._suffix = suffix - async def update(self, attr: AttrR): - response = await self.controller.connection.send_query( - f"{self.command_name}{self.controller.suffix}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}{self._suffix}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any): - await self.controller.connection.send_command( - f"{self.command_name}{self.controller.suffix}={attr.dtype(value)}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}{self._suffix}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class OnOffEnum(enum.StrEnum): @@ -53,47 +54,44 @@ class OnOffEnum(enum.StrEnum): class TemperatureRampController(Controller): - start = AttrRW(Int(), handler=TemperatureControllerHandler("S")) - end = AttrRW(Int(), handler=TemperatureControllerHandler("E")) - enabled = AttrRW(Enum(OnOffEnum), handler=TemperatureControllerHandler("N")) - target = AttrR(Float(), handler=TemperatureControllerHandler("T")) - actual = AttrR(Float(), handler=TemperatureControllerHandler("A")) + start = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="S")) + end = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="E")) + enabled = AttrRW(Enum(OnOffEnum), io_ref=TemperatureControllerAttributeIORef("N")) + target = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("T")) + actual = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("A")) voltage = AttrR(Float()) - def __init__(self, index: int, connection: IPConnection): - self.suffix = f"{index:02d}" - - super().__init__(f"Ramp{self.suffix}") - - self.connection = connection + def __init__(self, index: int, connection: IPConnection) -> None: + suffix = f"{index:02d}" + super().__init__( + f"Ramp{suffix}", ios=[TemperatureControllerAttributeIO(connection, suffix)] + ) class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerHandler("ID")) - power = AttrR(Float(), handler=TemperatureControllerHandler("P")) - ramp_rate = AttrRW(Float(), handler=TemperatureControllerHandler("R")) - - suffix = "" + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) + ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef("R")) def __init__(self, ramp_count: int, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) self._ramp_controllers: list[TemperatureRampController] = [] - for idx in range(1, ramp_count + 1): - ramp_controller = TemperatureRampController(idx, self.connection) - self._ramp_controllers.append(ramp_controller) - self.register_sub_controller(f"R{idx}", ramp_controller) + for index in range(1, ramp_count + 1): + controller = TemperatureRampController(index, self._connection) + self._ramp_controllers.append(controller) + self.register_sub_controller(f"R{index}", controller) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) @scan(0.1) async def update_voltages(self): voltages = json.loads( - (await self.connection.send_query("V?\r\n")).strip("\r\n") + (await self._connection.send_query("V?\r\n")).strip("\r\n") ) for index, controller in enumerate(self._ramp_controllers): await controller.voltage.set(float(voltages[index])) @@ -102,13 +100,11 @@ async def update_voltages(self): gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(4, connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(4, connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/snippets/static13.py b/docs/snippets/static13.py index aa5abaef2..7c3ae78df 100644 --- a/docs/snippets/static13.py +++ b/docs/snippets/static13.py @@ -1,51 +1,52 @@ -from __future__ import annotations - import asyncio import enum import json -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import Any +from typing import TypeVar -from fastcs.attributes import AttrHandlerRW, AttrR, AttrRW, AttrW +from fastcs.attribute_io import AttributeIO +from fastcs.attribute_io_ref import AttributeIORef +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings -from fastcs.controller import BaseController, Controller +from fastcs.controller import Controller from fastcs.datatypes import Enum, Float, Int, String from fastcs.launch import FastCS -from fastcs.transport.epics.ca.options import EpicsCAOptions +from fastcs.transport.epics.ca.transport import EpicsCATransport from fastcs.transport.epics.options import EpicsGUIOptions, EpicsIOCOptions from fastcs.wrappers import command, scan +NumberT = TypeVar("NumberT", int, float) + @dataclass -class TemperatureControllerHandler(AttrHandlerRW): - command_name: str +class TemperatureControllerAttributeIORef(AttributeIORef): + name: str + _: KW_ONLY update_period: float | None = 0.2 - _controller: TemperatureController | TemperatureRampController | None = None - async def initialise(self, controller: BaseController): - assert isinstance(controller, TemperatureController | TemperatureRampController) - self._controller = controller - @property - def controller(self) -> TemperatureController | TemperatureRampController: - if self._controller is None: - raise RuntimeError("Handler not initialised") +class TemperatureControllerAttributeIO( + AttributeIO[NumberT, TemperatureControllerAttributeIORef] +): + def __init__(self, connection: IPConnection, suffix: str = ""): + super().__init__() - return self._controller + self._connection = connection + self._suffix = suffix - async def update(self, attr: AttrR) -> None: - response = await self.controller.connection.send_query( - f"{self.command_name}{self.controller.suffix}?\r\n" - ) + async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]): + query = f"{attr.io_ref.name}{self._suffix}?" + response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") await attr.set(attr.dtype(value)) - async def put(self, attr: AttrW, value: Any) -> None: - await self.controller.connection.send_command( - f"{self.command_name}{self.controller.suffix}={attr.dtype(value)}\r\n" - ) + async def send( + self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT + ) -> None: + command = f"{attr.io_ref.name}{self._suffix}={attr.dtype(value)}" + await self._connection.send_command(f"{command}\r\n") class OnOffEnum(enum.StrEnum): @@ -54,47 +55,44 @@ class OnOffEnum(enum.StrEnum): class TemperatureRampController(Controller): - start = AttrRW(Int(), handler=TemperatureControllerHandler("S")) - end = AttrRW(Int(), handler=TemperatureControllerHandler("E")) - enabled = AttrRW(Enum(OnOffEnum), handler=TemperatureControllerHandler("N")) - target = AttrR(Float(), handler=TemperatureControllerHandler("T")) - actual = AttrR(Float(), handler=TemperatureControllerHandler("A")) + start = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="S")) + end = AttrRW(Int(), io_ref=TemperatureControllerAttributeIORef(name="E")) + enabled = AttrRW(Enum(OnOffEnum), io_ref=TemperatureControllerAttributeIORef("N")) + target = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("T")) + actual = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("A")) voltage = AttrR(Float()) - def __init__(self, index: int, connection: IPConnection): - self.suffix = f"{index:02d}" - - super().__init__(f"Ramp{self.suffix}") - - self.connection = connection + def __init__(self, index: int, connection: IPConnection) -> None: + suffix = f"{index:02d}" + super().__init__( + f"Ramp{suffix}", ios=[TemperatureControllerAttributeIO(connection, suffix)] + ) class TemperatureController(Controller): - device_id = AttrR(String(), handler=TemperatureControllerHandler("ID")) - power = AttrR(Float(), handler=TemperatureControllerHandler("P")) - ramp_rate = AttrRW(Float(), handler=TemperatureControllerHandler("R")) - - suffix = "" + device_id = AttrR(String(), io_ref=TemperatureControllerAttributeIORef("ID")) + power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef("P")) + ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef("R")) def __init__(self, ramp_count: int, settings: IPConnectionSettings): - super().__init__() - self._ip_settings = settings - self.connection = IPConnection() + self._connection = IPConnection() + + super().__init__(ios=[TemperatureControllerAttributeIO(self._connection)]) self._ramp_controllers: list[TemperatureRampController] = [] - for idx in range(1, ramp_count + 1): - ramp_controller = TemperatureRampController(idx, self.connection) - self._ramp_controllers.append(ramp_controller) - self.register_sub_controller(f"R{idx}", ramp_controller) + for index in range(1, ramp_count + 1): + controller = TemperatureRampController(index, self._connection) + self._ramp_controllers.append(controller) + self.register_sub_controller(f"R{index}", controller) async def connect(self): - await self.connection.connect(self._ip_settings) + await self._connection.connect(self._ip_settings) @scan(0.1) async def update_voltages(self): voltages = json.loads( - (await self.connection.send_query("V?\r\n")).strip("\r\n") + (await self._connection.send_query("V?\r\n")).strip("\r\n") ) for index, controller in enumerate(self._ramp_controllers): await controller.voltage.set(float(voltages[index])) @@ -110,13 +108,11 @@ async def disable_all(self) -> None: gui_options = EpicsGUIOptions( output_path=Path(".") / "demo.bob", title="Demo Temperature Controller" ) -epics_options = EpicsCAOptions( - gui=gui_options, - ca_ioc=EpicsIOCOptions(pv_prefix="DEMO"), -) +epics_ca = EpicsCATransport(gui=gui_options, ca_ioc=EpicsIOCOptions(pv_prefix="DEMO")) connection_settings = IPConnectionSettings("localhost", 25565) -fastcs = FastCS(TemperatureController(4, connection_settings), [epics_options]) +fastcs = FastCS(TemperatureController(4, connection_settings), [epics_ca]) fastcs.create_gui() -# fastcs.run() # Commented as this will block +if __name__ == "__main__": + fastcs.run() diff --git a/docs/tutorials/dynamic-drivers.md b/docs/tutorials/dynamic-drivers.md index 3e4393467..8b02b1e7b 100644 --- a/docs/tutorials/dynamic-drivers.md +++ b/docs/tutorials/dynamic-drivers.md @@ -43,29 +43,26 @@ implement an `initialise` method to create these dynamically instead. Create a pydantic model to validate the response from the device :::{literalinclude} /snippets/dynamic.py -:lines: 3-5,14-33 +:lines: 5,18-35 ::: -Create a function to parse the dictionary and validate the entries against the model +Create a function to parse the dictionary, validate the entries against the model and +create `Attributes`. :::{literalinclude} /snippets/dynamic.py -:lines: 36-54 +:lines: 38-56 ::: Update the controllers to not define attributes statically and implement initialise methods to create these attributes dynamically. :::{literalinclude} /snippets/dynamic.py -:lines: 76,81-83 -::: - -:::{literalinclude} /snippets/dynamic.py -:lines: 86,95-109 +:lines: 91-131 ::: The `suffix` field should also be removed from `TemperatureController` and -`TemperatureRampController` and then not used in `TemperatureControllerHandler` because -the `command` field on `TemperatureControllerParameter` includes this. +`TemperatureRampController` and then not used in `TemperatureControllerAttributeIO` +because the `command` field on `TemperatureControllerParameter` includes this. TODO: Add `enabled` back in to `TemperatureRampController` and recreate `disable_all` to demonstrate validation of introspected Attributes. diff --git a/docs/tutorials/static-drivers.md b/docs/tutorials/static-drivers.md index c478e02d0..ebb1019af 100644 --- a/docs/tutorials/static-drivers.md +++ b/docs/tutorials/static-drivers.md @@ -3,8 +3,8 @@ ## Demo Simulation Within FastCS there is a tickit simulation of a temperature controller. Clone the FastCS -repository and open it in VS Code. The simulation can be run with the -`Temp Controller Sim` launch config by typing `Ctrl+P debug ` (note the trailing +repository and open it in VS Code. The simulation can be run with the +`Temp Controller Sim` launch config by typing `Ctrl+P debug ` (note the trailing whitespace), selecting the launch config and pressing enter. The simulation will then sit and wait for commands to be sent. When it receives commands, it will log them to the console to show what it is doing. @@ -44,13 +44,13 @@ along with an empty list of transports (for now). :class: dropdown, hint :::{literalinclude} /snippets/static02.py -:emphasize-lines: 2,9,10 +:emphasize-lines: 2,9,11-12 ::: :::: Now the application runs, but it still doesn't expose any API because the `Controller` -is empty. It also completes immediately. +is empty. ## FastCS Attributes @@ -69,9 +69,25 @@ is a string, so `String` should be used. :::: -Now the controller has a property that will appear in the API, but the application still -completes immediately because there are no transports being run on the event loop to -expose an API. +Now the controller has a property that will appear in the API, but there are no +transports being run on the event loop to expose that API. The controller can be +interacted with in the console, but note that it hasn't populated any values because it +doesn't have a connection. + +::::{admonition} Interactive Shell +:class: dropdown, hint + +::: +In [1]: controller.device_id + +Out[1]: AttrR(String()) + +In [2]: controller.device_id.get() + +Out[2]: '' +::: + +:::: ## FastCS Transports @@ -85,7 +101,7 @@ following transports are currently supported - HTTP (using `fastapi`) One or more of these can be loaded into the application and run in parallel. Add the -EPICS CA transport to the application by creating an `EPICSCAOptions` instance and +EPICS CA transport to the application by creating an `EPICSCATransport` instance and passing it in. ::::{admonition} Code 4 @@ -97,14 +113,9 @@ passing it in. :::: -:::{warning} -In the above snippet and all hereafter, the final line is commented out. This is done to -avoid blocking our unit tests - in your own code, it should remain uncommented. -::: - -The application will now run until it is stopped. There will also be a `DEMO:DeviceId` -PV being served by the application. However, the record is unset because the -`Controller` is not yet querying the simulator for the value. +There will now be a `DEMO:DeviceId` PV being served by the application. However, the +record is unset because the `Controller` is not yet querying the simulator for the +value. ```bash ❯ caget -S DEMO:DeviceId @@ -118,7 +129,7 @@ options to the transport options and generate a `demo.bob` file to use with Phoe :class: dropdown, hint :::{literalinclude} /snippets/static05.py -:emphasize-lines: 1,7,15-21,24 +:emphasize-lines: 1,7,16-20,22 ::: :::: @@ -128,9 +139,8 @@ The `demo.bob` will have been created in the directory the application was run f ## FastCS Device Connection The `Attributes` of a FastCS `Controller` need some IO with the device in order to get -and set values. This is implemented with handlers and connections. There are three types -of handler - `Sender`, `Updater` and `Handler` (which implements both). Generally each -driver implements its own connection logic, but there are some built in options. +and set values. This is implemented with `AttributeIO`s and connections. Generally each +driver implements its own IO and connection logic, but there are some built in options. Update the controller to create an `IPConnection` to communicate with the simulator over TCP and implement a `connect` method that establishes the connection. The `connect` @@ -145,7 +155,7 @@ The simulator control connection is on port 25565. :class: dropdown, hint :::{literalinclude} /snippets/static06.py -:emphasize-lines: 4,15-22,32-33 +:emphasize-lines: 4,15-22,29-30 ::: :::: @@ -155,10 +165,11 @@ The application will now fail to connect if the demo simulation is not running. ::: The `Controller` has now established a connection with the simulator. This connection -can be passed to an `Updater` to enable it to query the device API and update the value -in the `device_id` attribute. Create an `Updater` child class and implement the method -to send a query to the device and set the value of the attribute, and then pass an -instance of the `Updater` to the `device_id` attribute. +can be passed to an `AttributeIO` to enable it to query the device API and update the +value in the `device_id` attribute. Create a `TemperatureControllerAttributeIO` child +class and implement the `update` method to query the device and set the value of the +attribute, and create a `TemperatureControllerAttributeIORef` and pass an instance of +it to the `device_id` attribute to tell the controller what io to use to update it. :::{note} The `update_period` property tells the base class how often to call `update` @@ -168,7 +179,7 @@ The `update_period` property tells the base class how often to call `update` :class: dropdown, hint :::{literalinclude} /snippets/static07.py -:emphasize-lines: 1-3,6,8,15-35,39 +:emphasize-lines: 1,3,5-6,15-33,37,43 ::: :::: @@ -190,13 +201,12 @@ DEMO:DeviceId SIMTCONT123 The simulator supports many other commands, for example it reports the total power currently being drawn with the `P` command. This can be exposed by adding another -`AttrR` with a `Float` datatype, but the `IDUpdater` only supports the `ID` command to -get the device ID. This new attribute could have its own updater, but it would be better -to create common updater that can be configured with the command to send so that they -can share the same code. +`AttrR` with a `Float` datatype, but the IO only supports the `ID` command to get the +device ID. This new attribute could have its own IO, but it is similar enough that the +existing IO can be support both. -Modify the handler to take a `command_name` string and use it to create a new attribute -to read the power usage. +Modify the IO ref to take a `name` string and update the IO to use it in the query +string sent to the device. Create a new attribute to read the power usage using this. :::{note} All responses from the `IPConnection` are strings. This is fine for the `ID` command @@ -210,7 +220,7 @@ etc. :class: dropdown, hint :::{literalinclude} /snippets/static08.py -:emphasize-lines: 9,16-17,33-35,38,42-43 +:emphasize-lines: 10,19-21,33-38,42-43 ::: :::: @@ -219,12 +229,11 @@ Now the IOC has two PVs being polled periodically. The new PV will be visible in Phoebus UI on refresh (right-click). `DEMO:Power` will read as `0` because the simulator is not currently running a ramp. To do that the controller needs to be able to set values on the device, as well as read them back. The ramp rate of the temperature can be -read with the `R` command and set with the `R=...` command. This means the controller -also needs a `Sender` handler that implements `put` to send values to the device. +read with the `R` command and set with the `R=...` command. This means the IO also needs +a `send` method to send values to the device. -Update the existing handler to support both `update` and `put`, making it a `Handler` -(which implements both `Updater` and `Sender`). Then add a new `AttrRW` with type -`Float` to get and set the ramp rate. +Update the IO to implement `send` and then add a new `AttrRW` with type `Float` to get +and set the ramp rate. :::{note} The set commands do not return a response, so use the `send_command` method instead of @@ -235,7 +244,7 @@ The set commands do not return a response, so use the `send_command` method inst :class: dropdown, hint :::{literalinclude} /snippets/static09.py -:emphasize-lines: 5,7,17,41-44, 48-50 +:emphasize-lines: 7,40-44,48-50 ::: :::: @@ -270,17 +279,16 @@ has. This can be done with the use of sub controllers. Controllers can be arbitr nested to match the structure of a device and this structure is then mirrored to the transport layer for the visibility of the user. -Create a `TemperatureRampController` with `AttrRW`s to get and set the ramp start and -end, update the `TemperatureControllerHandler` to include an optional suffix for the -commands so that it can be shared between the parent `TemperatureController` and add an -argument to define how many ramps there are, which is used to register the correct -number of ramp controllers with the parent. +Create a `TemperatureRampController` with two `AttrRW`s the ramp start and end, update +the IO to include an optional suffix for the commands so that it can be shared with +the parent `TemperatureController` and add an argument to define how many ramps there +are, which is used to register the correct number of ramp controllers with the parent. ::::{admonition} Code 10 :class: dropdown, hint :::{literalinclude} /snippets/static10.py -:emphasize-lines: 9,10,20,23,27,35,43,47-56,64,66,72-76,90 +:emphasize-lines: 10,28,32,35,44,48-56,64,70-74,85 ::: :::: @@ -305,7 +313,7 @@ Add an `AttrRW` to the `TemperatureRampController`s with an `Enum` type, using a :class: dropdown, hint :::{literalinclude} /snippets/static11.py -:emphasize-lines: 3,11,48-50,56 +:emphasize-lines: 1,11,49-51,57 ::: :::: @@ -347,10 +355,10 @@ The applied voltage for each ramp is also available with the `V?` command, but t is an array with each element corresponding to a ramp. Here it will be simplest to manually fetch the array in the parent controller and pass each value into ramp controller. This can be done with a `scan` method - these are called at a defined rate, -similar to the `update` method of handlers. +similar to the `update` method of an `AttributeIO`. -Add an `AttrR` for the voltage to the `TemperatureRampController`, but do not pass it a -handler. Then add a method to the `TemperatureController` with a `@scan` decorator that +Add an `AttrR` for the voltage to the `TemperatureRampController`, but do not pass it an +IO ref. Then add a method to the `TemperatureController` with a `@scan` decorator that gets the array of voltages and sets each ramp controller with its value. Also add `AttrR`s for the target and actual temperature for each ramp as described above. @@ -358,14 +366,14 @@ gets the array of voltages and sets each ramp controller with its value. Also ad :class: dropdown, hint :::{literalinclude} /snippets/static12.py -:emphasize-lines: 4,16,59-61,93-99 +:emphasize-lines: 2,16,60-62,91-97 ::: :::: Creating attributes is intended to be a simple API covering most use cases, but where more flexibility is needed wrapped controller methods can be useful to avoid adding -complexity to the handlers to handle a small subset of attributes. It is also useful for +complexity to the IO to handle a small subset of attributes. It is also useful for implementing higher level logic on top of the attributes that expose the API of a device directly. For example, it would be useful to have a single button to stop all of the ramps at the same time. This can be done with a `command` method. These are similar to @@ -379,7 +387,7 @@ controller. :class: dropdown, hint :::{literalinclude} /snippets/static13.py -:emphasize-lines: 3,17,102-108 +:emphasize-lines: 1,17,100-105 ::: :::: @@ -401,4 +409,4 @@ DEMO:R1:Enabled_RBV Off This demonstrates some of the simple use cases for a statically defined FastCS driver. It is also possible to instantiate a driver dynamically by instantiating a device during -startup. See the next tutorial for how to do this (TODO). +startup. See the next tutorial for how to do this. diff --git a/src/fastcs/attribute_io.py b/src/fastcs/attribute_io.py index ba182356c..fbf30d7b9 100644 --- a/src/fastcs/attribute_io.py +++ b/src/fastcs/attribute_io.py @@ -26,6 +26,9 @@ def __init_subclass__(cls) -> None: args = get_args(cast(Any, cls).__orig_bases__[0]) cls.ref_type = args[1] + def __init__(self): + super().__init__() + async def update(self, attr: AttrR[T, AttributeIORefT]) -> None: raise NotImplementedError() diff --git a/src/fastcs/attribute_io_ref.py b/src/fastcs/attribute_io_ref.py index d71059eb1..21e61e451 100644 --- a/src/fastcs/attribute_io_ref.py +++ b/src/fastcs/attribute_io_ref.py @@ -1,9 +1,9 @@ -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from typing_extensions import TypeVar -@dataclass(kw_only=True) +@dataclass class AttributeIORef: """Base for references to define IO for an ``Attribute`` over an API. @@ -14,6 +14,8 @@ class AttributeIORef: server. """ + # Make fields keyword-only so that child classes can have fields without defaults + _: KW_ONLY update_period: float | None = None diff --git a/src/fastcs/demo/controllers.py b/src/fastcs/demo/controllers.py index 12fade2bf..28098787c 100755 --- a/src/fastcs/demo/controllers.py +++ b/src/fastcs/demo/controllers.py @@ -1,9 +1,7 @@ -from __future__ import annotations - import asyncio import enum import json -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from typing import TypeVar from fastcs.attribute_io import AttributeIO @@ -28,20 +26,19 @@ class TemperatureControllerSettings: ip_settings: IPConnectionSettings -@dataclass(kw_only=True) +@dataclass class TemperatureControllerAttributeIORef(AttributeIORef): name: str + _: KW_ONLY update_period: float | None = 0.2 - def __post_init__(self): - # Call __init__ of non-dataclass parent - super().__init__() - class TemperatureControllerAttributeIO( AttributeIO[NumberT, TemperatureControllerAttributeIORef] ): def __init__(self, connection: IPConnection, suffix: str): + super().__init__() + self._connection = connection self.suffix = suffix @@ -56,7 +53,7 @@ async def update( self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] ) -> None: query = f"{attr.io_ref.name}{self.suffix}?" - response = await self._connection.send_query(f"{query}?\r\n") + response = await self._connection.send_query(f"{query}\r\n") response = response.strip("\r\n") self.log_event( "Query for attribute", diff --git a/tests/test_docs_snippets.py b/tests/test_docs_snippets.py index 5b3f24ec9..3d2aaaf5c 100644 --- a/tests/test_docs_snippets.py +++ b/tests/test_docs_snippets.py @@ -37,7 +37,6 @@ def sim_temperature_controller(): print(process.communicate()[0]) -@pytest.mark.skip("Skipping docs tests until docs snippets are updated") @pytest.mark.parametrize("filename", glob.glob("docs/snippets/*.py", recursive=True)) def test_snippet(filename): runpy.run_path(filename)