Source code for fastcs.controller

from __future__ import annotations

import asyncio
from copy import deepcopy
from typing import get_type_hints

from fastcs.attributes import Attribute


[docs] class BaseController: """Base class for controller.""" #: Attributes passed from the device at runtime. attributes: dict[str, Attribute] description: str | None = None def __init__( self, path: list[str] | None = None, description: str | None = None ) -> None: if ( description is not None ): # Use the argument over the one class defined description. self.description = description if not hasattr(self, "attributes"): self.attributes = {} self._path: list[str] = path or [] self.__sub_controller_tree: dict[str, SubController] = {} self._bind_attrs() async def initialise(self): pass async def attribute_initialise(self) -> None: # Initialise any registered handlers for attributes coros = [attr.initialise(self) for attr in self.attributes.values()] try: await asyncio.gather(*coros) except asyncio.CancelledError: pass for controller in self.get_sub_controllers().values(): await controller.attribute_initialise() @property def path(self) -> list[str]: """Path prefix of attributes, recursively including parent Controllers.""" return self._path def set_path(self, path: list[str]): if self._path: raise ValueError(f"SubController is already registered under {self.path}") self._path = path def _bind_attrs(self) -> None: """Search for `Attributes` and `Methods` to bind them to this instance. This method will search the attributes of this controller class to bind them to this specific instance. For `Attribute`s, this is just a case of copying and re-assigning to `self` to make it unique across multiple instances of this controller class. For `Method`s, this requires creating a bound method from a class method and a controller instance, so that it can be called from any context with the controller instance passed as the `self` argument. """ # Lazy import to avoid circular references from fastcs.cs_methods import UnboundCommand, UnboundPut, UnboundScan # Using a dictionary instead of a set to maintain order. class_dir = {key: None for key in dir(type(self)) if not key.startswith("_")} class_type_hints = { key: value for key, value in get_type_hints(type(self)).items() if not key.startswith("_") } for attr_name in {**class_dir, **class_type_hints}: if attr_name == "root_attribute": continue attr = getattr(self, attr_name, None) if isinstance(attr, Attribute): if ( attr_name in self.attributes and self.attributes[attr_name] is not attr ): raise ValueError( f"`{type(self).__name__}` has conflicting attribute " f"`{attr_name}` already present in the attributes dict." ) new_attribute = deepcopy(attr) setattr(self, attr_name, new_attribute) self.attributes[attr_name] = new_attribute elif isinstance(attr, UnboundPut | UnboundScan | UnboundCommand): setattr(self, attr_name, attr.bind(self)) def register_sub_controller(self, name: str, sub_controller: SubController): if name in self.__sub_controller_tree.keys(): raise ValueError( f"Controller {self} already has a SubController registered as {name}" ) self.__sub_controller_tree[name] = sub_controller sub_controller.set_path(self.path + [name]) if isinstance(sub_controller.root_attribute, Attribute): if name in self.attributes: raise TypeError( f"Cannot set SubController `{name}` root attribute " f"on the parent controller `{type(self).__name__}` " f"as it already has an attribute of that name." ) self.attributes[name] = sub_controller.root_attribute def get_sub_controllers(self) -> dict[str, SubController]: return self.__sub_controller_tree
[docs] class Controller(BaseController): """Top-level controller for a device. This is the primary class for implementing device support in FastCS. Instances of this class can be loaded into a backend to access its ``Attribute``s. The backend can then perform a specific function with the set of ``Attributes``, such as generating a UI or creating parameters for a control system. """ def __init__(self, description: str | None = None) -> None: super().__init__(description=description) async def connect(self) -> None: pass
[docs] class SubController(BaseController): """A subordinate to a ``Controller`` for managing a subset of a device. An instance of this class can be registered with a parent ``Controller`` to include it as part of a larger device. """ root_attribute: Attribute | None = None def __init__(self, description: str | None = None) -> None: super().__init__(description=description)