Source code for ska_tango_base.future._heritable_admin_mode

#
# This file is part of the SKA Tango Base project
#
# Distributed under the terms of the BSD 3-clause new license.
# See LICENSE.txt for more info.
"""A device with an heritable admin mode."""

from __future__ import annotations

import concurrent.futures as _fs
import inspect
import logging
import time
import types
import typing

import ska_control_model as scm
import tango.server

from .. import type_hints, utils
from ..callback_scheduler import CallbackID, CallbackScheduler, CallbackSchedulerMixin
from ..software_bus import AttrSignal, SharingObserver, Signal, attribute_from_signal
from ._base_interface import BaseInterface


[docs] class InheritType: """Type of the Inherit marker object.""" pass
Inherit = InheritType() """The inherit marker object.""" _INHERIT_VALUE = len(scm.future.AdminMode.__members__) class _AdminModeInheritor(SharingObserver): def __init__( self, parent_trl: str, connection_timeout: float, *, logger: logging.Logger | None = None, time_source: typing.Callable[[], float] = time.time, ) -> None: self._parent_trl = parent_trl # We store the future for _callback_id so that we only # wait for this if we actually need to use it to unregister. self._callback_id: _fs.Future[CallbackID] | None = None self._disconnect_timestamp: float | None = None self._connection_timeout = connection_timeout self._time_source = time_source self._last_admin_mode_from_parent: scm.future.AdminMode | None = None if logger is None: self._logger = logging.getLogger(__name__) else: self._logger = logger def start(self, scheduler: CallbackScheduler) -> None: if self._callback_id is None: self._callback_id = scheduler.register_event_callback( self._parent_trl, "adminMode", tango.EventType.CHANGE_EVENT, self.on_parent_admin_mode, ) def stop(self, scheduler: CallbackScheduler) -> None: if self._callback_id is not None: try: scheduler.unregister_callback(self._callback_id.result()) except Exception: pass finally: self._callback_id = None self._last_admin_mode_from_parent = None def on_parent_admin_mode(self, event: type_hints.EventDataType) -> None: """Process a adminMode event from parent.""" if event.err: # NOTE: Here we assume any error event is a connection issue, in # principle the parent device could send a custom error, so this # assumption is not really correct. However, without a use case for # pushing these custom events I struggle to know what I should do # instead. self._logger.warning( "Received error event when subscribing to parent adminMode: %s", event.errors, ) self.notify_parent_not_connected() else: self.update_admin_mode(event.attr_value.value) def update_admin_mode( self, admin_mode: scm.future.AdminMode, ) -> None: self._disconnect_timestamp = None self.shared_bus.emit("._admin_mode", admin_mode) if self._callback_id is None: self.shared_bus.emit( "._parent_connection_status", scm.CommunicationStatus.DISABLED ) else: self._last_admin_mode_from_parent = admin_mode self.shared_bus.emit( "._parent_connection_status", scm.CommunicationStatus.ESTABLISHED ) def notify_parent_not_connected(self) -> None: if self._disconnect_timestamp is None: self._disconnect_timestamp = self._time_source() if ( self._last_admin_mode_from_parent is not None and self._time_source() - self._disconnect_timestamp < self._connection_timeout ): # We deliberately emit another adminMode event here in case # the write value has changed self.shared_bus.emit("._admin_mode", self._last_admin_mode_from_parent) self.shared_bus.emit( "._parent_connection_status", scm.CommunicationStatus.NOT_ESTABLISHED ) else: self.shared_bus.emit("._admin_mode", None) self.shared_bus.emit( "._parent_connection_status", ( scm.CommunicationStatus.NOT_ESTABLISHED, None, tango.AttrQuality.ATTR_ALARM, ), )
[docs] class HeritableAdminModeMixin(CallbackSchedulerMixin, BaseInterface): ParentTRL = tango.server.device_property( mandatory=False, default_value="", dtype="DevString" ) """ Tango resource locator of parent. If not set, or an empty string, the device is considered to not have a parent and adminMode inheritance is not possible. """ ParentConnectionTimeout = tango.server.device_property( mandatory=False, default_value=60, dtype="DevFloat" ) """Seconds until lack of contact from parent results in an alarm."""
[docs] def init_device(self) -> None: """Initialise device.""" super().init_device() # We don't want write_adminMode to wait around for event subscriptions # to be set up. To achieve this we explicitly connect from the # device_exported_hook thread and use this variable to keep track of # whether that has completed yet. # # Lock invariant for Tango monitor: # - If this is set and we are inheriting adminMode, then the # _AdminModeInheritor has been started. self._parent_admin_mode_connected = False if self.ParentTRL: # We will get some flickering here during a call to Init() with e.g. # ENGINEERING memorized, but I think that is acceptable. self.adminMode.set_write_value(_INHERIT_VALUE) self.write_adminMode(Inherit) else: self.adminMode.set_write_value(scm.future.AdminMode.ENGINEERING) self.write_adminMode(scm.future.AdminMode.ENGINEERING)
[docs] def on_new_shared_bus(self) -> None: """Create adminMode inheritor.""" super().on_new_shared_bus() self._admin_mode_inheritor = _AdminModeInheritor( self.ParentTRL, self.ParentConnectionTimeout, logger=self.logger )
[docs] def device_exported_hook(self) -> None: """Connect to parent.""" super().device_exported_hook() if self.ParentTRL: self.callback_scheduler.connect_event_stream( self.ParentTRL, "adminMode", tango.EventType.CHANGE_EVENT ) with utils._hold_tango_monitor(self): self._parent_admin_mode_connected = True if self.adminMode.get_write_value() == _INHERIT_VALUE: self._admin_mode_inheritor.start(self.callback_scheduler)
@tango.server.attribute(dtype="DevString", doc=ParentTRL.__doc__) def parentTRL(self) -> str: """Return the configured TRL of the parent.""" return typing.cast(str, self.ParentTRL) @tango.server.attribute(dtype="DevFloat", doc=ParentConnectionTimeout.__doc__) def parentConnectionTimeout(self) -> float: """Return the configured parent timeout.""" return typing.cast(float, self.ParentConnectionTimeout) _parent_connection_status = AttrSignal[scm.CommunicationStatus]( stored=True, initial_value=scm.CommunicationStatus.DISABLED ) parentConnectionStatus = attribute_from_signal( _parent_connection_status, dtype=scm.CommunicationStatus, doc="Communication status to parent device.", ) """Attribute reporting the connection status to the parent.""" _admin_mode = Signal[scm.future.AdminMode](stored=True) """Override _admin_mode signal to remove initial value. This doc string is not marked public, so _admin_mode will be hidden from the documentation. The fact we are overriding this is not of interest to subclasses. """ adminMode = attribute_from_signal( _admin_mode, dtype="DevEnum", enum_labels=list(scm.future.AdminMode.__members__.keys()) + ["INHERIT"], description=( "The Admin Mode of the device.\n\n" "Set by a client to mark the device for science or only engineering." "May be set to a special 'INHERIT' value to automatically update the " "adminMode from the parent at parentTRL. Cannot set to 'INHERIT' if " "parentTRL is empty or not set." ), memorized=True, hw_memorized=True, ) """Overridden admin mode attribute with additional INHERIT set point.""" def __write_adminMode(self, value: int) -> None: converted: scm.future.AdminMode | InheritType if value == _INHERIT_VALUE: converted = Inherit else: converted = scm.future.AdminMode(value) self.write_adminMode(converted) adminMode.write(__write_adminMode) @tango.server.attribute( dtype="DevBoolean", description=("True if the device's adminMode is being inherited"), ) def inheritAdminMode(self) -> bool: """Return True if the device is inheriting its adminMode.""" return typing.cast(bool, self.adminMode.get_write_value() == _INHERIT_VALUE)
[docs] def write_adminMode(self, setpoint: scm.future.AdminMode | InheritType) -> None: """ Set the Admin Mode of the device. :param setpoint: The value to set it to :raises tango.DevFailed: When trying to set INHERIT with no ParentTRL """ if setpoint is Inherit: if not self.ParentTRL: info = inspect.getframeinfo( typing.cast(types.FrameType, inspect.currentframe()) ) tango.Except.throw_exception( "STB_NotSupported", "'INHERIT' is not supported: No ParentTRL set", f"{info.filename}:{info.lineno}", ) self._admin_mode_inheritor.stop(self.callback_scheduler) if self._parent_admin_mode_connected: # This may end up calling notify_parent_not_connected() # if an error event is passed to on_parent_admin_mode(), # however, if we have good data then we go straight to # ESTABLISHED. self._admin_mode_inheritor.start(self.callback_scheduler) else: self._admin_mode_inheritor.notify_parent_not_connected() else: self._admin_mode_inheritor.stop(self.callback_scheduler) self._admin_mode_inheritor.update_admin_mode( typing.cast(scm.future.AdminMode, setpoint) )