.. warning:: ska-tango-base 1.7.0 has not been released yet - this migration guide is still a work in progress. ================ Migrating to 1.7 ================ ska-tango-base 1.7.0 introduces a new ``ska_tango_base.future`` subpackage that provides a simplified, opinionated API intended to replace the existing base class hierarchy in a future major release. This guide describes how to migrate devices that currently inherit from :py:class:`~ska_tango_base.base.base_device.SKABaseDevice`, :py:class:`~ska_tango_base.obs.obs_device.SKAObsDevice`, :py:class:`~ska_tango_base.subarray.subarray_device.SKASubarray`, or :py:class:`~ska_tango_base.controller.controller_device.SKAController` to the corresponding ``future`` interfaces. Both APIs can coexist in the same project during migration. .. contents:: Contents :depth: 2 :local: :backlinks: none Class inheritance ----------------- The following table shows the direct class equivalents: .. list-table:: :header-rows: 1 :widths: 40 40 * - Old class - New class * - ``ska_tango_base.base.SKABaseDevice`` - :py:class:`~ska_tango_base.future._base_interface.BaseInterface` * - ``ska_tango_base.obs.SKAObsDevice`` - :py:class:`~ska_tango_base.future._obs_interface.ObsInterface` * - ``ska_tango_base.subarray.SKASubarray`` - :py:class:`~ska_tango_base.future._subarray_interface.SubarrayInterface` * - ``ska_tango_base.controller.SKAController`` - :py:class:`~ska_tango_base.future._controller_interface.ControllerInterface` :py:class:`~ska_tango_base.future._powered_interface.PoweredInterface` is new. It sits between :py:class:`~ska_tango_base.future._base_interface.BaseInterface` and :py:class:`~ska_tango_base.future._obs_interface.ObsInterface` and should be used whenever the device controls a component that has power states (ON/OFF/STANDBY). Example class inheritance with the old API: .. code:: python from ska_tango_base import SKABaseDevice class MyDevice(SKABaseDevice["MyComponentManager"]): InitCommand = None ... New class inheritance needed for the same device with the new API: .. code:: python from ska_tango_base import future # Needs to imported explicitly class MyDevice( future.ComponentManagerLRCMixin["MyComponentManager"], future.BaseInterface, ): ... For devices that control a powered component: .. code:: python from ska_tango_base import future # Needs to imported explicitly class MyDevice( future.ComponentManagerLRCMixin["MyComponentManager"], future.PoweredInterface, ): ... :py:class:`~ska_tango_base.future._component_manager_mixins.ComponentManagerLRCMixin` is a convenience mixin that combines component-manager attachment with LRC support. Use :py:class:`~ska_tango_base.future._component_manager_mixins.ComponentManagerMixin` instead if you do not need LRCs. InitCommand removed ------------------- The ``InitCommand`` class object pattern has been removed. A device using ska-tango-base 1.4 or newer should have already acknowledged the deprecation by assigning ``InitCommand = None`` and overriding :py:meth:`~tango.server.Device.init_device` directly. There is no need to set ``InitCommand = None`` with the future classes, but it does no harm if to leave it in. Component manager ----------------- Using the :py:class:`~ska_tango_base.base.base_component_manager.BaseComponentManager` with :py:class:`~ska_tango_base.base.base_device.SKABaseDevice` requires ``communication_state_callback`` and ``component_state_callback`` constructor arguments and uses :py:class:`~ska_control_model.CommunicationStatus` to gate the Tango device operational state. The new API removes this concept entirely. Component managers should instead inherit from the appropriate observer mixin and call the appropriate state update methods directly: .. list-table:: :header-rows: 1 :widths: 40 40 * - Old base class - New base class * - ``BaseComponentManager`` - :py:class:`~ska_tango_base.future._base_interface.OpStateEmitMixin` * - *(powered variant)* - :py:class:`~ska_tango_base.future._powered_interface.PoweredOpStateEmitMixin` * - ``SubarrayComponentManager`` - :py:class:`~ska_tango_base.future._obs_interface.ObsStateEmitMixin` To retain the task executor, also inherit from :py:class:`~ska_tango_base.executor.executor_component_manager.TaskExecutorComponentManager`: .. code:: python import ska_tango_base as stb import ska_control_model as scm # Old class MyComponentManager(stb.base.BaseComponentManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def on(self, task_callback=None): return self._task_executor.submit(self._do_on, task_callback=task_callback) def _communication_state_changed(self, communication_state): # old pattern — not needed in the new API ... # New class MyComponentManager( stb.future.PoweredOpStateEmitMixin, stb.executor.TaskExecutorComponentManager, ): @stb.executor.TaskExecutor.task def do_on(self, progress_callback, task_abort_event): self.component_on() return scm.ResultCode.OK, "On completed" The key differences: * No constructor arguments for state callbacks — the signal bus propagates state automatically once the component manager is attached to the device via :py:class:`~ska_tango_base.future._component_manager_mixins.ComponentManagerMixin`. * No :py:class:`~ska_control_model.CommunicationStatus`. The device starts in ``INIT`` state and transitions to ``ON`` (or the appropriate power state) as soon as :py:meth:`~ska_tango_base.future._base_interface.BaseInterface.init_completed` is called. State management ---------------- The state machine objects (``op_state_model``, ``obs_state_model``) are removed as decided with ADR-124. State is now driven entirely by calling the appropriate state update methods on either the device or the component manager, which will automatically update the Tango device state and send change and archive events as needed. The mapping from method arguments to resulting Tango device state is as follows: .. list-table:: :header-rows: 1 :widths: 30 35 35 * - Interface - Method - Resulting ``DevState`` * - :py:class:`~ska_tango_base.future._base_interface.BaseInterface` - ``init_invoked()`` - ``INIT`` * - :py:class:`~ska_tango_base.future._base_interface.BaseInterface` - ``init_completed()`` - ``ON`` * - :py:class:`~ska_tango_base.future._base_interface.BaseInterface` - ``software_fault("")`` - ``FAULT`` * - :py:class:`~ska_tango_base.future._powered_interface.PoweredInterface` - ``component_on()`` - ``ON`` * - :py:class:`~ska_tango_base.future._powered_interface.PoweredInterface` - ``component_standby()`` - ``STANDBY`` * - :py:class:`~ska_tango_base.future._powered_interface.PoweredInterface` - ``component_off()`` - ``OFF`` Similarly, for the observation state (:py:class:`~ska_control_model.ObsState`), the appropriate state update methods should be called directly: .. code:: python self.component_resourced() # → ObsState.IDLE self.component_configured() # → ObsState.READY self.component_scanning() # → ObsState.SCANNING self.component_obsfault() # → ObsState.FAULT Device initialisation --------------------- For :py:class:`~ska_tango_base.future._base_interface.BaseInterface` (and all subclasses) the initialisation sequence is: 1. ``super().init_device()`` — sets the device to ``INIT`` state. 2. Set up any device-specific state. 3. ``self.init_completed()`` — transitions the device out of ``INIT``. If using a component manager via :py:class:`~ska_tango_base.future._component_manager_mixins.ComponentManagerMixin`, the component manager is created automatically inside ``on_new_shared_bus`` (before ``init_device`` returns from the ``super()`` call). You do **not** need to call ``create_component_manager()`` yourself. For :py:class:`~ska_tango_base.future._powered_interface.PoweredInterface` the device does not enter ``ON`` state automatically after :py:meth:`~ska_tango_base.future._base_interface.BaseInterface.init_completed`. Instead, explicitly set the power state by calling either :py:meth:`~ska_tango_base.future._powered_interface.PoweredOpStateEmitMixin.component_on`, :py:meth:`~ska_tango_base.future._powered_interface.PoweredOpStateEmitMixin.component_standby` or :py:meth:`~ska_tango_base.future._powered_interface.PoweredOpStateEmitMixin.component_off` before calling :py:meth:`~ska_tango_base.future._base_interface.BaseInterface.init_completed`: .. code:: python def init_device(self) -> None: super().init_device() # Establish the power state and then call init_completed() self.component_off() self.init_completed() Optional attributes: controlMode, simulationMode, testMode ---------------------------------------------------------- The optional attributes ``controlMode``, ``simulationMode``, and ``testMode`` are no longer included by default. Add them explicitly using the factory functions, which return a ``(signal, attribute)`` pair to unpack at class scope: .. code:: python import ska_tango_base as stb class MyDevice(stb.future.BaseInterface): _control_mode, controlMode = stb.future.standard_control_mode() _simulation_mode, simulationMode = stb.future.standard_simulation_mode() _test_mode, testMode = stb.future.standard_test_mode() These factory functions are also available at :py:func:`~ska_tango_base.base.base_interface.standard_control_mode`, :py:func:`~ska_tango_base.base.base_interface.standard_simulation_mode`, and :py:func:`~ska_tango_base.base.base_interface.standard_test_mode` for use with the old API (unchanged since 1.4). Optional attribute: obsMode --------------------------- The :py:attr:`~ska_tango_base.obs.obs_interface.ObsInterface.obsMode` attribute is not included by default in :py:class:`~ska_tango_base.future._obs_interface.ObsInterface`. Add it with: .. code:: python class MyObsDevice(stb.future.ObsInterface): _obs_mode, obsMode = stb.future.standard_obs_mode() Long running commands --------------------- The LRC pattern is unchanged. The ``@submit_lrc_task`` and ``@long_running_command`` decorators work identically on the new interfaces. The ``execute_`` override pattern is the same: .. code:: python import ska_tango_base as stb class MyDevice( stb.future.ComponentManagerLRCMixin["MyComponentManager"], stb.future.PoweredInterface, ): @stb.long_running_commands.submit_lrc_task def execute_On(self) -> stb.type_hints.TaskFunctionType: return self.component_manager.do_on @stb.long_running_commands.submit_lrc_task def execute_Off(self) -> stb.type_hints.TaskFunctionType: return self.component_manager.do_off Subarray devices ---------------- Replace :py:class:`~ska_tango_base.subarray.subarray_device.SKASubarray` with :py:class:`~ska_tango_base.future._subarray_interface.SubarrayInterface`. All subarray commands (``AssignResources``, ``ReleaseResources``, ``Configure``, ``Scan``, ``EndScan``, ``End``, ``Abort``, ``ObsReset``, ``Restart``, ``ReleaseAllResources``) are already defined on the interface and enabled once the corresponding ``execute_`` method is overridden. Unlike the old API, the obsState transitions during command execution are handled automatically by the interface via ``started_`` / ``completed_`` callbacks — **you do not need to set** ``_obs_state`` in your ``execute_`` methods. Override ``started_`` or ``completed_`` only if the default transition is wrong for your use case. .. code:: python import ska_tango_base as stb class MySubarray( stb.future.ComponentManagerLRCMixin["MySubarrayComponentManager"], stb.future.SubarrayInterface, ): def create_component_manager(self): return MySubarrayComponentManager(self.logger) def read_assignedResources(self) -> list[str]: return self.component_manager.assigned_resources def read_configuredCapabilities(self) -> list[str]: return self.component_manager.configured_capabilities @stb.long_running_commands.submit_lrc_task def execute_AssignResources(self, argin: str): resources = set(json.loads(argin).get("resources", [])) return functools.partial(self.component_manager.do_assign, resources) The ``Abort`` command does not have a corresponding ``execute_Abort`` override. Override :py:meth:`~ska_tango_base.long_running_commands.mixin.AbstractLRCMixin.schedule_abort_task` instead, as in the old API. Controller devices ------------------ Replace :py:class:`~ska_tango_base.controller.controller_device.SKAController` or the old :py:class:`~ska_tango_base.controller.controller_interface.ControllerInterface` with the new :py:class:`~ska_tango_base.future._controller_interface.ControllerInterface`. The ``MaxCapabilities`` device property and the ``maxCapabilities``, ``availableCapabilities``, and ``IsCapabilityAchievable`` interface members are provided unchanged. Full example ------------ A minimal powered device with a component manager and LRCs: .. code:: python import ska_control_model as scm import ska_tango_base as stb from ska_tango_base import future # Needs to imported explicitly class MyComponentManager( future.PoweredOpStateEmitMixin, stb.executor.TaskExecutorComponentManager, ): def _on_unhandled_exception(self, exception: Exception) -> None: self.software_fault(str(exception)) @stb.executor.TaskExecutor.task def do_on(self, progress_callback, task_abort_event): ... # talk to hardware self.component_on() return scm.ResultCode.OK, "On completed" @stb.executor.TaskExecutor.task def do_off(self, progress_callback, task_abort_event): ... self.component_off() return scm.ResultCode.OK, "Off completed" class MyDevice( future.ComponentManagerLRCMixin["MyComponentManager"], future.PoweredInterface, ): _control_mode, controlMode = future.standard_control_mode() _simulation_mode, simulationMode = future.standard_simulation_mode() _test_mode, testMode = future.standard_test_mode() def create_component_manager(self) -> MyComponentManager: return MyComponentManager(self.logger) def init_device(self) -> None: super().init_device() self.component_off() self.init_completed() @stb.long_running_commands.submit_lrc_task def execute_On(self) -> stb.type_hints.TaskFunctionType: return self.component_manager.do_on @stb.long_running_commands.submit_lrc_task def execute_Off(self) -> stb.type_hints.TaskFunctionType: return self.component_manager.do_off def report_health_from_monitoring(self) -> None: # Called from your monitoring loop issues = ... if issues: self.report_health(scm.HealthState.DEGRADED, issues) else: self.report_health(scm.HealthState.OK, [])