.. _init-guidelines: ================================== Initialising your SKA Tango device ================================== Prefer init_device to __init__ ------------------------------ .. note :: :py:class:`~.SKABaseDevice` provides a deprecated mechanism to provide device initialisation by overriding an :py:class:`~.SKABaseDevice.InitCommand` object. In order to use :code:`init_device` as described here you must set :py:class:`!InitCommand` to :code:`None` to acknowledge the deprecation. See :ref:`migrate-1-4-cmd-obj` for more details. In general, initialisation of a Tango device should be done in the :py:func:`!init_device` method as this allows clients to "re-initialise" the device via the built-in :py:func:`!Init` command. For example, the following device starts a background thread and opens a socket connect .. code-block:: py class MyDevice(LRCMixin, SKADevice): HardwareHost = device_property(dtype=str, mandatory=True) HardwarePort = device_property(dtype=int, mandatory=True) def init_device(self) -> None: # device properties are read from the Tango database here: super().init_device() # TODO: Handle connection failure self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.HardwareHost, self.HardwarePort)) self.stop_event = threading.Event # To signal to the background thread to stop self.background_thread = threading.Thread(target=self._run_background_thread) self.background_thread.start() def _run_background_thread(self) -> None: # All threads should be made an omnithread: with tango.EnsureOmniThread(): ... .. tip:: Don't forget to call :code:`super().init_device()`! Typically, this should be done *before* your code. Clean up resources in delete_device ----------------------------------- All resources allocated during :py:func:`!init_device` should be reclaimed during the :py:func:`!delete_device` method. In other words, all threads must be joined, files/sockets closed, Tango event subscriptions must be unsubscribed, etc. As we are using python, we don't have to worry about the python objects themselves being reclaimed as this will be handled by the python interpreter automatically. .. warning:: Caution must be taken when relying on the python interpreter to reclaim PyTango objects as the C++ destructor will run for these objects, which may do things beyond just reclaiming memory. For example, if a :class:`tango.DeviceProxy` is destroyed all Tango event subscriptions that were created by that device proxy will be unsubscribed. If the :class:`tango.DeviceProxy` was being kept alive by a complex python object with reference cycles this unsubscription will be done at an unknown time by the python garbage collector. For the example device above, we need to join the thread and close the socket: .. code:: python class MyDevice(LRCMixin, SKADevice): ... def delete_device(self) -> None: self.stop_event.set() with self.allow_internal_threads(): self.background_thread.join() self.socket.close() super().delete_device() .. tip:: Don't forget to call :code:`super().delete_device()`! Typically, this should be done *after* your code. .. tip:: When joining a thread that needs to interact with the Tango device, use :py:func:`~ska_tango_base.software_bus.SignalBusMixin.allow_internal_threads` to avoid a deadlock. This will release the Tango device monitor lock, allowing the thread being joined to acquire the monitor lock, but it will not allow new client requests to start. Provide an on_new_shared_bus method when initialising objects that use the bus ------------------------------------------------------------------------------ The :meth:`~.SharingObserver.on_new_shared_bus` method is called after the signal bus object has been created, but before :py:class:`~.SharingObserver` sub-objects are located and before the signal bus thread is started. This is the correct time to create :py:class:`~.SharingObserver` sub-objects, or objects that want to use the signal bus. For example, the following device server adds dynamic :py:class:`~.attribute_from_signal` objects: .. code:: python class MyDevice(LRCMixin, SKADevice): ... my_signal = Signal[int](stored=True, initial_value=0) def on_new_shared_bus(self) -> None: super().on_new_shared_bus() self.add_attribute(attribute_from_signal(self.__class__.my_signal)) .. tip:: Don't forget to call :code:`super().on_new_shared_bus()`! Typically, this should be done *before* your code. Call init_completed when inheriting from SKABaseDevice ------------------------------------------------------ When inheriting from :py:class:`~.SKABaseDevice`, the :code:`super().init_device()` will set the device state to :code:`INIT`. You are then responsible for calling :meth:`~.SKABaseDevice.init_completed()` once initialisation has finished. This will then transition the device state according to the operational state model. It is possible to call :meth:`~.SKABaseDevice.init_completed()` some time later if your device has a long running initialisation process. For example, the following device calls :meth:`~.SKABaseDevice.init_completed()` immediately from its :py:func:`!init_device`: .. code:: python class MyDevice(SKABaseDevice[MyComponentManager]): InitCommand = None def init_device(self) -> None: super().init_device() self.init_completed()