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 SKA<X> classes that implement one of the <X>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 SKABaseDevice class implements the BaseInterface interface by delegating to a subclass of 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 Components and component managers.
Warning
In the ska-tango-base 1.4.0 release, the <X>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 <X>interface classes directly as described in Writing your first SKA Tango device using ska-tango-base 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.
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 <X>Interface class:
These form a hierarchy that mimics the hierarchy of the <X>Interface classes:
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 <X>Interface you intend to implement.
Each of these base classes is abstract as various methods are not implemented. For example, the on method is as follows:
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, BaseComponentManager provides an _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 _update_component_state() method is called with the new PowerState as the power keyword argument. This will call the component_state_callback passed to BaseComponentManager.__init__ with the supplied keyword arguments. How to manage the 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:
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 aSignal;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 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 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:
The
on()command should be implemented to tell the component to power up. If the component accepts this command without complaint, then theon()command should return success. The component manager should not, however, assume that the component is now on.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):
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 create_component_manager() method to return a newly constructed instance of your component manager class:
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 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 _update_communication_state(), which internally calls the supplied communication_state_callback. The derived component manager will then call _update_communication_state() when overriding start_communicating and 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 _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 <type>_state_changed() method.
Communication state - Perform actions based on the input communication_state
(CommunicationStatus type from
ska_control_model).
Method:
_communication_state_changed()Minimum example:
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 (PowerState type from ska_control_model).
Method:
_component_state_changed()Minimum example:
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 Signal and then provide the attributes on your Tango device using attribute_from_signal. For example, for a temperature attribute:
class MyComponentManager(SharingObserver, BaseComponentManager):
...
temperature = AttrSignal[float]()
...
class MyDevice(SKABaseDevice[MyComponentManager]):
...
temperature = attribute_from_signal("component_manager.temperature", dtype=float)
...
Components may be software instead of hardware. In this case the SKABaseDevice._component_state_changed method should still be called with a power argument to drive the op_state_model, e.g. PowerState.ON to arrive in the ON operational state. Alternatively SKABaseDevice._communication_state_changed could be overridden so that a communication_state of CommunicationStatus.ESTABLISHED instead transitions the operational state to DevState.ON. See Components which have only a single power state 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 SKA command objects deprecated for details about how to migrate from the command object implementation. Notably, the deprecation of the InitCommand command object should be ackowledged by setting InitCommand = None and ensuring that init_completed() is called in init_device(), as described in the migration guide.
For commands provided by the Tango interface you are inheriting from, you must override the execute_<X>() method. For example, to provide the behaviour for the On() command:
def execute_On(self) -> DevVarLongStringArrayType:
return self.component_manager.on()
For additional commands beyond what is provided by the Tango interface, use the standard tango.server.command decorator:
@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 TaskFunctionType and you should use the long_running_command() and submit_lrc_task() decorators as appropriate. For example,
@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 How to implement a long running command for more details about long running commands.
Note
The commands provided by SKASubarray should be implemented as long running commands, whereas, the commands provided by SKABaseDevice can be implemented as either long running or fast commands.