============================================== How to write a device with a component manager ============================================== A fundamental assumption of the ``ska-tango-base`` package is that each Tango device exists to provide monitoring and control of some *component* of a SKA telescope. That *component* could be some hardware, a software service or process, or even a group of subservient Tango devices. The package provides a collection of :class:`!SKA` classes that implement one of the :class:`!interface` classes by delegating the behaviour of the device to a so-called component manager. With this implementation strategy the Tango device itself is just a thin wrapper around the component manager. For example, the :class:`~ska_tango_base.base.base_device.SKABaseDevice` class implements the :class:`~ska_tango_base.base.base_interface.BaseInterface` interface by delegating to a subclass of :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager`. The advantage of this implementation strategy is that the component manager can be tested independently of Tango, allowing more granular unit testing. This guide describes how to write a Tango device using a component manager with ``ska-tango-base``. For more information on components and component managers, see :doc:`../concepts/component-managers`. .. warning :: In the ``ska-tango-base`` 1.4.0 release, the :class:`!interface` classes were factored out from the collection of classes described above in order to provide an more modular API. Although the implementation strategy of delegating behaviour to some other python object to enable unit testing is reasonable, the best way to organise this is often application specific. It has proved difficult for a foundational library to provide a universally applicable API to this implementation strategy, and so, the 1.4.0 release introduced a more modular API to allow teams to pick and choose which pieces of ``ska-tango-base`` are useful for them and to allow the maintainers of ``ska-tango-base`` to more easily evolve the API. There are no plans to remove component manager support from the library at this time, however, it is not recommended to use these classes for new devices. Instead it is recommended to use the :class:`!interface` classes directly as described in :doc:`../tutorials/create-tango-device` using the delegating implementation strategy as appropriate for your application. This guide is aimed at developers maintaining existing Tango devices that use the component manager classes by describing the things their device should be doing in order to use ``ska-tango-base`` with a component manager as intended. .. contents:: Steps :depth: 2 :local: :backlinks: none Step 1: Write a component manager --------------------------------- Your component manager is going to implement the behaviour of your Tango device. It is recommended to implement and test this before introducing your Tango device. Step 1.1: Choose a subclass for your component manager ====================================================== There are several component manager base classes, each associated with a Tango :class:`!Interface` class: - :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager` - :class:`~ska_tango_base.controller.controller_device.ControllerComponentManager` - :class:`~ska_tango_base.obs.obs_device.ObsDeviceComponentManager` - :class:`~ska_tango_base.subarray.subarray_component_manager.SubarrayComponentManager` These form a hierarchy that mimics the hierarchy of the :class:`!Interface` classes: .. uml :: @startuml hide empty members abstract class BaseComponentManager { } abstract class ControllerComponentManager { } abstract class ObsDeviceComponentManager { } abstract class SubarrayComponentManager { } ObsDeviceComponentManager <|- SubarrayComponentManager : inherits BaseComponentManager <|- ObsDeviceComponentManager : inherits BaseComponentManager <|-- ControllerComponentManager: inherits @enduml The first step to building a Tango device with a component manager is to pick which of these base classes for your component manager to inherit from. This should be based on the :class:`!Interface` you intend to implement. Each of these base classes is abstract as various methods are not implemented. For example, the :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager.on` method is as follows: .. code :: python def on( self: BaseComponentManager, task_callback: TaskCallbackType | None = None ) -> tuple[TaskStatus, str]: raise NotImplementedError( f"'on' method must be implemented by '{self.__class__.__name__}'. " "The parent 'BaseComponentManager' is an abstract base class." ) .. warning :: Unfortunately, it may be required for your component manager to change the interface of these methods in order to, for example, implement them as long running commands using the 1.4.0 API. This may result in linting errors as your subclass does not obey the "Listov substitution rules" for subclassing. It is safe to ignore these errors as, with the new API, these methods are only ever called by your Tango device. Once your component manager has inherited from one of the base component managers you should override these methods with concrete implementations. Step 1.2: Establish communication with your component ===================================================== How you do this will depend on the capabilities and interface of your component. For example: * If the component interface is via a connection-oriented protocol (such as TCP/IP), then the component manager must establish and maintain a *connection* to the component; * If the component is able to publish updates, then the component manager would need to subscribe to those updates; * If the component cannot publish updates, but can only respond to requests, then the component manager would need to initiate polling of the component. Step 1.3: Implement component monitoring ======================================== Whenever your component changes its state, your component manager needs to become reliably aware of that change within a reasonable time frame, so that it can pass this on to the Tango device. The abstract component managers provided already contain some helper methods to trigger device callbacks. For example, :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager` provides an :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager._update_component_state` method that triggers callbacks to indicate that the power level of the component has changed. You need to implement component monitoring so that, if the component's power level changes, the :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager._update_component_state` method is called with the new :class:`~ska_control_model.PowerState` as the ``power`` keyword argument. This will call the ``component_state_callback`` passed to :meth:`BaseComponentManager.__init__ ` with the supplied keyword arguments. :doc:`operational-state` provides more details about managing the power level. For component-specific functionality, you will need to implement the corresponding helper methods. For example, if your component reports its temperature, then your component manager will need to: 1. Implement a mechanism by which it can let its Tango device know that the component temperature has changed, such as by providing a callback, extending the existing ``component_state_callback``, or using a :class:`~ska_tango_base.software_bus.Signal`; 2. Implement monitoring so that this mechanism is triggered whenever a change in component temperature is detected. Step 1.4: Implement component control ===================================== Methods to control the component must be implemented; for example the component manager's :meth:`!on()` method must be implemented to actually tell the component to turn on. Note that component *control* and component *monitoring* are decoupled from each other. So a component manager's :meth:`!on()` method should not directly call the callback that tells the device that the component is now on. Rather, the command should return without calling the callback, and leave it to the *monitoring* to detect when the component has changed states. Consider a component that takes ten seconds to power up: 1. The ``on()`` command should be implemented to tell the component to power up. If the component accepts this command without complaint, then the ``on()`` command should return success. The component manager should not, however, assume that the component is now on. 2. After ten seconds, the component has powered up, and the component manager's monitoring detects that the component is on. Only then should the callback be called to let the device know that the component has changed state, resulting in a change of device state to ``ON``. .. note:: A component manager may maintain additional state, and support additional commands, that do not map to its component. That is, a call to a component manager needs not always result in a call to the underlying component. For example, a subarray's component manager may implement its ``assign_resources`` method simply to maintain a record (within the component manager itself) of what resources it has, so that it can validate arguments to other methods (for example, check that arguments to its ``configure`` method do not require access to resources that have not been assigned to it). In this case, the call to the component manager's ``assign_resources`` method would not result in interaction with the component; indeed, the component may not even possess the concepts of *resources* and *resource assignment*. Step 2: Write your Tango device ------------------------------- Once you have a component manager, your next step is to write a Tango device that delegates its behaviour to the component manager. This involves the following steps: Step 2.1: Select a device class to subclass =========================================== This should correspond to the base class you used for your component manager. The base classes form the following hierarchy and each implements a particular Tango interface (the hierarchy of the interfaces has been omitted for clarity): .. uml :: @startuml hide empty members interface BaseInterface { } interface ControllerInterface { } interface ObsInterface { } interface SubarrayInterface { } abstract class SKABaseDevice { } abstract class SKAController { } abstract class SKAObsDevice { } abstract class SKASubarray { } BaseInterface <|- SKABaseDevice : implements ControllerInterface <|- SKAController : implements SKAObsDevice --|> ObsInterface: implements SKASubarray --|> SubarrayInterface: implements SKAObsDevice <|- SKASubarray : inherits SKABaseDevice <|- SKAObsDevice : inherits SKABaseDevice <|-- SKAController: inherits @enduml Each of these base Tango device classes is abstract because the subclass must register their component manager. Step 2.2: Register your component manager ========================================= Your Tango device must override the :meth:`~ska_tango_base.base.base_device.SKABaseDevice.create_component_manager` method to return a newly constructed instance of your component manager class: .. code-block:: py def create_component_manager(self): return MyComponentManager( logger=self.logger, communication_state_callback=self._communication_state_callback, component_state_callback=self._component_state_callback ) When instantiating a component manager object that inherits directly or indirectly from :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager`, two callbacks should be provided by the Tango device, one for the communication state and the other for the component state. These callbacks allow the component manager to inform the Tango device of changes to the component while staying decoupled from the device itself. For instance, the derived component manager class will inherit :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager._update_communication_state`, which internally calls the supplied ``communication_state_callback``. The derived component manager will then call :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager._update_communication_state` when overriding :class:`~ska_tango_base.base.base_component_manager.BaseComponentManager.start_communicating` and :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager.stop_communicating` to drive the ``op_state_model``. Similarly, when the component manager determines that the power state of the component has changed, it will use :meth:`~ska_tango_base.base.base_component_manager.BaseComponentManager._update_component_state` to drive the ``op_state_model``. At a minimum, to ensure the actions are performed on the ``op_state_model``, both callbacks should call the relevant :meth:`!_state_changed` method. **Communication state** - Perform actions based on the input ``communication_state`` (:obj:`CommunicationStatus ` type from ska_control_model). * Method: :meth:`~ska_tango_base.base.base_device.SKABaseDevice._communication_state_changed` * Minimum example: .. code-block:: py def _communication_state_callback( communication_state: CommunicationStatus ) -> None: super()._communication_state_changed(communication_state) **Component state** - Perform actions based on the input fault (boolean) and/or power (:obj:`PowerState ` type from ska_control_model). * Method: :meth:`~ska_tango_base.base.base_device.SKABaseDevice._component_state_changed` * Minimum example: .. code-block:: py def _component_state_callback( fault: Optional[bool] = None, power: Optional[PowerState] = None ) -> None: super()._component_state_changed(fault=fault, power=power) Step 2.3: Implement attributes ============================== Information the component manager retrieves from the component needs to be made available to the wider control system in the form of attributes on the Tango device. This can be done with the use of additional keyword arguments provided to the callbacks above that are used to update said attributes. However, a device with many different sensors could then end up with a long list of keyword arguments, and a cumbersome callback. It is recommended to split these additional arguments into their own callback(s) by adding methods to the Tango device with corresponding parameters in the derived component manager. Alternatively, your component manager could provide values for the additional attributes as a :class:`~ska_tango_base.software_bus.Signal` and then provide the attributes on your Tango device using :class:`~ska_tango_base.software_bus.attribute_from_signal`. For example, for a ``temperature`` attribute: .. code :: python class MyComponentManager(SharingObserver, BaseComponentManager): ... temperature = AttrSignal[float]() ... class MyDevice(SKABaseDevice[MyComponentManager]): ... temperature = attribute_from_signal("component_manager.temperature", dtype=float) ... .. TODO: More detailed how-to about attribute_from_signal Components may be software instead of hardware. In this case the :meth:`SKABaseDevice._component_state_changed ` method should still be called with a power argument to drive the ``op_state_model``, e.g. :class:`PowerState.ON ` to arrive in the ON operational state. Alternatively :meth:`SKABaseDevice._communication_state_changed ` could be overridden so that a ``communication_state`` of :class:`CommunicationStatus.ESTABLISHED ` instead transitions the operational state to :class:`DevState.ON `. See :ref:`always-on-component` for more details. Step 2.4: Implement commands ============================ The control methods provided by your component manager need to be exposed as Tango commands. .. warning :: By default the component-manager-based Tango devices use the deprecated SKA command objects. The steps outlined here do not use this mechanism. See :ref:`migrate-1-4-cmd-obj` for details about how to migrate from the command object implementation. Notably, the deprecation of the :class:`~ska_tango_base.base.base_device.SKABaseDevice.InitCommand` command object should be ackowledged by setting `InitCommand = None` and ensuring that :meth:`~ska_tango_base.base.base_interface.BaseInterface.init_completed()` is called in :meth:`~ska_tango_base.base.base_device.SKABaseDevice.init_device()`, as described in the migration guide. For commands provided by the Tango interface you are inheriting from, you must override the :meth:`!execute_` method. For example, to provide the behaviour for the :meth:`!On()` command: .. code:: python def execute_On(self) -> DevVarLongStringArrayType: return self.component_manager.on() For additional commands beyond what is provided by the Tango interface, use the standard :class:`tango.server.command` decorator: .. code:: python @tango.server.command def MyCmd(self) -> DevVarLongStringArrayType: return self.component_manager.my_cmd() If you wish for your command to be a long running command, your component manager methods should be implemented as :class:`~ska_tango_base.type_hints.TaskFunctionType` and you should use the :meth:`~ska_tango_base.long_running_commands.decorators.long_running_command` and :meth:`~ska_tango_base.long_running_commands.decorators.submit_lrc_task` decorators as appropriate. For example, .. code:: python @submit_lrc_task def execute_On(self) -> TaskFunctionType: return self.component_manager.on @long_running_command def MyCmd(self) -> TaskFunctionType: return self.component_manager.my_cmd See :doc:`add-an-lrc` for more details about long running commands. .. note :: The commands provided by :class:`~ska_tango_base.subarray.subarray_device.SKASubarray` should be implemented as long running commands, whereas, the commands provided by :class:`~ska_tango_base.base.base_device.SKABaseDevice` can be implemented as either long running or fast commands.