diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json index 5b7465a5b52eb7dce306cf5bb69eaf7d108d3c17..ff3a7b7d11f93f1fc3e4754d75a6cc625d61e1b0 100644 --- a/CDB/LOFAR_ConfigDb.json +++ b/CDB/LOFAR_ConfigDb.json @@ -14,6 +14,13 @@ } } }, + "station_control": { + "LTS": { + "StationControl": { + "LTS/StationControl/1": {} + } + } + }, "RECV": { "LTS": { "RECV": { diff --git a/devices/devices/station_control.py b/devices/devices/station_control.py new file mode 100644 index 0000000000000000000000000000000000000000..27bca1714ef409f3a6c18730c04b1bc86c7a144b --- /dev/null +++ b/devices/devices/station_control.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the RECV project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" StationControl Device Server for LOFAR2.0 + +""" + +# TODO(Corne): Remove sys.path.append hack once packaging is in place! +import os, sys +currentdir = os.path.dirname(os.path.realpath(__file__)) +parentdir = os.path.dirname(currentdir) +sys.path.append(parentdir) + +# PyTango imports +from tango import DebugIt +from tango.server import run, command +from tango.server import device_property, attribute +from tango import AttrWriteType, DeviceProxy, DevState +# Additional import + +from device_decorators import * + +from clients.attribute_wrapper import attribute_wrapper +from devices.hardware_device import hardware_device +from common.lofar_logging import device_logging_to_python, log_exceptions +from common.lofar_git import get_version + +__all__ = ["StationControl", "main"] + + +class InitialisationException(Exception): + pass + +@device_logging_to_python() +class StationControl(hardware_device): + """ + + **Properties:** + + - Device Property + OPC_Server_Name + - Type:'DevString' + OPC_Server_Port + - Type:'DevULong' + OPC_Time_Out + - Type:'DevDouble' + """ + + # ----------------- + # Device Properties + # ----------------- + + DeviceProxy_Time_Out = device_property( + dtype='DevDouble', + mandatory=False, + default=3.0, + ) + + # By default, we assume any device is not available + # because its docker container was not started, which + # is an explicit and thus intentional action. + # We ignore such devices when initialising the station. + Ignore_Unavailable_Devices = device_property( + dtype='DevBoolean', + mandatory=False, + default=True, + ) + + # ---------- + # Attributes + # ---------- + version_R = attribute(dtype=str, access=AttrWriteType.READ, fget=lambda self: get_version()) + initialising_station_R = attribute(dtype=numpy.bool_, access=AttrWriteType.READ, fget=lambda self: self.initialising_station) + initialisation_progress_R = attribute(dtype=numpy.int, access=AttrWriteType.READ, fget=lambda self: numpy.int(self.initialisation_progress)) + + @log_exceptions() + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # overloaded functions + # -------- + @log_exceptions() + def configure_for_off(self): + """ user code here. is called when the state is set to OFF """ + # Stop keep-alive + try: + pass + except Exception as e: + self.warn_stream("Exception while stopping OPC ua connection in configure_for_off function: {}. Exception ignored".format(e)) + + @log_exceptions() + def configure_for_initialise(self): + # all devices we're controlling + self.devices = { + "recv": DeviceProxy("LTS/RECV/1"), + "unb2": DeviceProxy("LTS/UNB2/1"), + + "sdp": DeviceProxy("LTS/SDP/1"), + "sst": DeviceProxy("LTS/SST/1"), + "xst": DeviceProxy("LTS/XST/1"), + + "docker": DeviceProxy("LTS/Docker/1"), + } + + # restart these before any others, and in this order + self.restart_first = [ + "docker", # needed to do a deep restart of devices in case of malfunction + "sdp", # we need the TR_fpga_mask to be set before starting the statistics devices + ] + + # set the timeout for all deviceproxies + for device in self.devices: + device.set_timeout_millis(self.DeviceProxy_Time_Out * 1000) + + # setup initial state + self.initialising_station = False + self.initialisation_progress = 0 + + @command() + @DebugIt() + @only_when_on() + @fault_on_error() + def initialise_devices(self): + """ + Initialise or re-initialise all devices on the station. + + This command will take a while to execute, so should be called asynchronously. + + :return:None + """ + + try: + # mark us as busy + self.set_state(DevState.RUNNING) + + # reset initialisation parameters + self.initialising_station = True + self.initialisation_progress = 0 + num_restarted_devices = 0 + + # determine initialisation order + devices_ordered = self.restart_first + [d for d in self.devices.keys() if d not in self.restart_first] + + # First, stop all devices, to get a well defined state + for device in devices_ordered: + if self.is_available(device) or not self.Ignore_Unavailable_Devices: + self.stop_device(device) + + # restart devices in order + for device in devices_ordered: + if self.is_available(device) or not self.Ignore_Unavailable_Devices: + self.start_device(device) + + num_restarted_devices += 1 + self.initialisation_progress = 100.0 * num_restarted_devices / len(self.devices) + + # make sure we always finish at 100% in case of success + self.initialisation_progress = 100 + + except InitialisationException as e: + logger.log_exception("Error initialising station") + + # Just because they go to FAULT, doesn't mean we have to. + # Note that the user can query the state of the devices from the devices themselves. + # The condition (initialisation_progress < 100 and not initialising_station) + # will be an indicator initialisation went wrong. + finally: + self.initialising_station = False + + # revert to ON, which is the state we were called in. if an Exception happened by now, + # we'll go to FAULT. Both are guaranteed by our decorators. + self.set_state(DevState.ON) + + def is_available(self, device_name: str): + """ Return whether the device 'device_name' is actually available on this server. """ + + proxy = self.devices[device_name] + try: + proxy.state() + except Exception as e: + return False + + return True + + @command() + @DebugIt() + @only_when_on() + def stop_device(self, device_name: str): + """ Stop device 'device_name'. """ + + if proxy.state() != DevState.OFF: + self.set_status(f"[restarting {device_name}] Turning off device.") + proxy.Off() + if proxy.state() != DevState.OFF: + raise InitialisationException(f"Could not turn off device {device_name}") + + + @command() + @DebugIt() + @only_when_on() + def start_device(self, device_name: str): + """ Run the startup sequence for device 'device_name'. """ + + proxy = self.devices[device_name] + + # go to a well-defined state, which may be needed if the user calls + # this function explicitly. + self.stop(device_name) + + # setup connections to hardware + self.set_status(f"[restarting {device_name}] Initialising device.") + proxy.Initialise() + if proxy.state() != DevState.INIT: + raise InitialisationException(f"Could not initialise device {device_name}") + + # configure the device + try: + self.set_status(f"[restarting {device_name}] Setting defaults.") + proxy.set_defaults() + + self.set_status(f"[restarting {device_name}] Initialising hardware.") + proxy.initialise_hardware() + except Exception as e: + raise InitialisationException(f"Could not configure device {device_name}") from e + + # mark as ready for service + self.set_status(f"[restarting {device_name}] Turning on device.") + proxy.On() + if proxy.state() != DevState.ON: + raise InitialisationException(f"Could not turn on device {device_name}") + + self.set_status(f"[restarting {device_name}] Succesfully restarted.") + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the RECV module.""" + + from common.lofar_logging import configure_logger + configure_logger() + + return run((StationControl,), args=args, **kwargs) + + +if __name__ == '__main__': + main() diff --git a/docker-compose/device-station_control.yml b/docker-compose/device-station_control.yml new file mode 100644 index 0000000000000000000000000000000000000000..02cf41e26dea31f90c1e294e61e5142b79044eed --- /dev/null +++ b/docker-compose/device-station_control.yml @@ -0,0 +1,41 @@ +# +# Docker compose file that launches a LOFAR2.0 station's +# ObservationControl device. It also runs the dynamically +# created Observation devices. +# +# Defines: +# - device-observation_control: LOFAR2.0 station ObvservationControl +# +# Requires: +# - lofar-device-base.yml +# +version: '2' + +services: + device-station_control: + image: device-station_control + # build explicitly, as docker-compose does not understand a local image + # being shared among services. + build: + context: lofar-device-base + args: + SOURCE_IMAGE: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}-tango-itango:${TANGO_ITANGO_VERSION} + container_name: ${CONTAINER_NAME_PREFIX}device-station_control + networks: + - control + ports: + - "5708:5708" # unique port for this DS + volumes: + - ${TANGO_LOFAR_CONTAINER_MOUNT} + environment: + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + # configure CORBA to _listen_ on 0:port, but tell others we're _reachable_ through ${HOSTNAME}:port, since CORBA + # can't know about our Docker port forwarding + - python3 -u ${TANGO_LOFAR_CONTAINER_DIR}/devices/devices/station_control.py LTS -v -ORBendPoint giop:tcp:0:5708 -ORBendPointPublish giop:tcp:${HOSTNAME}:5708 + restart: on-failure diff --git a/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py b/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py index df75d5962a1327041995aa04c41d6d1e1c2ae914..ba7ade76a915001e8ded8d7b3474c73fc2d25a1c 100644 --- a/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py +++ b/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py @@ -4,6 +4,7 @@ sdp = DeviceProxy("LTS/SDP/1") sst = DeviceProxy("LTS/SST/1") xst = DeviceProxy("LTS/XST/1") unb2 = DeviceProxy("LTS/UNB2/1") +sc = DeviceProxy("LTS/StationControl/1") # Put them in a list in case one wants to iterate -devices = [recv, sdp, sst, xst, unb2] +devices = [recv, sdp, sst, xst, unb2, sc]