Example #1
0
class DeviceExampleServer(katcp.DeviceServer):

    ## Interface version information.
    VERSION_INFO = ("example-server", 0, 1)

    ## Device server build / instance information.
    BUILD_INFO = ("my-example-server", 0, 1, "rc1")

    #pylint: disable-msg=R0904
    def setup_sensors(self):
        pass

    def request_echo(self, sock, msg):
        """Echo the arguments of the message sent."""
        return katcp.Message.reply(msg.name, "ok", *msg.arguments)

    @request(Str(), Int())
    @return_reply(Str())
    def request_repeat(self, sock, txt, n):
        """Repeat txt n times."""
        return ("ok", txt * n)

    @request(Float(), Float())
    @return_reply(Float())
    def request_add(self, sock, x, y):
        """Add x and y."""
        return ("ok", x + y)

    @request(Int(), Int())
    @return_reply(Int())
    def request_intdiv(self, sock, x, y):
        """Perform integer division of x and y."""
        return ("ok", x // y)
Example #2
0
    def setUp(self):
        basic = Int()
        default = Int(default=11)
        optional = Int(optional=True)
        default_optional = Int(default=11, optional=True)
        self.minmax = Int(min=5, max=6)

        self._pack = [
            (basic, 5, "5"),
            (basic, -5, "-5"),
            (basic, "a", TypeError),
            (basic, None, ValueError),
            (self.minmax, 5, "5"),
            (self.minmax, 6, "6"),
            (self.minmax, 4, ValueError),
            (self.minmax, 7, ValueError),
            (default, None, "11"),
            (default_optional, None, "11"),
            (optional, None, ValueError),
        ]

        self._unpack = [
            (basic, "5", 5),
            (basic, "-5", -5),
            (basic, "a", ValueError),
            (basic, None, ValueError),
            (self.minmax, "5", 5),
            (self.minmax, "6", 6),
            (self.minmax, "4", ValueError),
            (self.minmax, "7", ValueError),
            (default, None, 11),
            (default_optional, None, 11),
            (optional, None, None),
        ]
Example #3
0
class FakeHandlers(object):
    @request(Int(), Int())
    @return_reply(Int())
    def request_add_test(self, req, a, b):
        "Add numbers"
        req.inform(a * 2, b * 3)
        return ('ok', a + b)

    @request(Int(), Int())
    @return_reply(Float())
    @tornado.gen.coroutine
    def request_async_divide(self, req, a, b):
        "Divide numbers"
        req.inform(a / 2, b / 10)
        req.inform('polony-is-real meat')
        raise tornado.gen.Return(('ok', a / b))

    @tornado.testing.gen_test
    def test_request_handlers(self):
        yield self.fake_inspecting_client.connect()
        test_handlers = FakeHandlers()

        self.fake_inspecting_manager.add_request_handlers_object(test_handlers)
        reply, informs = yield self.fake_inspecting_client.simple_request(
            'add-test', 1, 5, mid='123')
        self.assertEqual(len(informs), 1)
        self.assertEqual(str(informs[0]), '#add-test[123] 2 15')
        self.assertEqual(str(reply), '!add-test[123] ok 6')
        reply, informs = yield self.fake_inspecting_client.simple_request(
            'async-divide', 7, 2, mid='112')
        self.assertEqual(len(informs), 2)
        self.assertEqual(str(informs[0]),
                         '#async-divide[112] {} {}'.format(7 / 2, 2 / 10))
        self.assertEqual(str(informs[1]),
                         '#async-divide[112] polony-is-real\\_meat')
Example #4
0
class ManagementNode(NodeServer):
    """Katcp server for cluster head nodes.

    Notes: This is the basis of the top level
           interface for FBF/APSUSE.
    """
    VERSION_INFO = ("reynard-managementnode-api", 0, 1)
    BUILD_INFO = ("reynard-managementnode-implementation", 0, 1, "rc1")

    def __init__(self, server_host, server_port, config):
        self._clients = {}
        super(ManagementNode, self).__init__(server_host, server_port, config)

    def start(self):
        """Start the server

        Based on the passed configuration object this is
        where the clients for suboridnates nodes will be
        set up.
        """
        super(ManagementNode, self).start()
        self.ioloop.add_callback(self._setup_clients)

    def _setup_clients(self):
        """Setup clients based on configuration object."""
        for node, port in self._config.NODES:
            name = '{node}-client'.format(node=node)
            self._add_client(name, '127.0.0.1', port)

    def _add_client(self, name, ip, port):
        """Add a named client."""
        if name in self._clients.keys():
            raise KeyError(
                "Client already exists with name '{name}'".format(name=name))
        client = KATCPClientResource(
            dict(name=name, address=(ip, port), controlled=True))
        client.start()
        self._clients[name] = client

    def _remove_client(self, name):
        """Remove a client by name."""
        if name not in self._clients.keys():
            raise KeyError(
                "No client exists with name '{name}'".format(name=name))
        self._clients[name].stop()
        self._clients[name].join()
        del self._clients[name]

    @request(Str(), Str(), Int())
    @return_reply(Str())
    def request_client_add(self, req, name, ip, port):
        """Add a new client."""
        try:
            self._add_client(name, ip, port)
        except KeyError, e:
            return ("fail", str(e))
        return ("ok", "added client")
Example #5
0
class PafBackendController(AsyncDeviceServer):
    VERSION_INFO = ("paf-backend-controller-api", 0, 1)
    BUILD_INFO = ("paf-backend-controller-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]

    def __init__(self, ip, port):
        super(PafBackendController, self).__init__(ip, port)

    def setup_sensors(self):
        self._device_status = Sensor.discrete(
            "device-status",
            description="Health status of PafBackendController",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.UNKNOWN)

        self.add_sensor(self._device_status)

    def start(self):
        super(PafBackendController, self).start()

    @request(Int(), Float(), Bool())
    @return_reply(Int(), Float())
    def request_myreq(self, req, my_int, my_float, my_bool):
        '''?myreq my_int my_float my_bool'''
        return ("ok", my_int + 1, my_float / 2.0)

    @request(Str())
    @return_reply(Str())
    def request_echo(self, req, message):
        """
        @brief      A request that echos a message
        """
        return ("ok", message)

    @request(Str())
    @return_reply(Str())
    def request_echomine(self, req, message):
        """
        @brief      A request that echos a message
        """
        return ("ok", message)
Example #6
0
    def setUp(self):
        basic = Int()
        default = Int(default=11)
        optional = Int(optional=True)
        default_optional = Int(default=11, optional=True)
        minmax = Int(min=5, max=6)
        big_minmax = Int(min=-2**64, max=2**64)

        self._pack = [
            (basic, 5, b"5"),
            (basic, -5, b"-5"),
            (basic, "a", TypeError),
            (basic, None, ValueError),
            (minmax, 5, b"5"),
            (minmax, 6, b"6"),
            (minmax, 4, ValueError),
            (minmax, 7, ValueError),
            (big_minmax, 2**64, b"18446744073709551616"),
            (big_minmax, -2**64, b"-18446744073709551616"),
            (big_minmax, 2**64 + 1, ValueError),
            (big_minmax, -2**64 - 1, ValueError),
            (default, None, b"11"),
            (default_optional, None, b"11"),
            (optional, None, ValueError),
        ]

        self._unpack = [
            (basic, b"5", 5),
            (basic, b"-5", -5),
            (basic, b"a", ValueError),
            (basic, None, ValueError),
            (minmax, b"5", 5),
            (minmax, b"6", 6),
            (minmax, b"4", ValueError),
            (minmax, b"7", ValueError),
            (default, None, 11),
            (default_optional, None, 11),
            (optional, None, None),
        ]
Example #7
0
class TestDevice(object):
    def __init__(self):
        self.sent_messages = []

    @request(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    @return_reply(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    def request_one(self, req, i, d, b):
        if i == 3:
            return ("fail", "I failed!")
        if i == 5:
            return ("bananas", "This should never be sent")
        if i == 6:
            return ("ok", i, d, b, "extra parameter")
        if i == 9:
            self.finish_request_one(req, i, d, b)
            raise AsyncReply()
        return ("ok", i, d, b)

    @send_reply(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    def finish_request_one(self, req, i, d, b):
        return (req, "ok", i, d, b)

    def reply(self, req, msg, orig_msg):
        self.sent_messages.append([req, msg])

    @request(Int(min=1, max=3, default=2),
             Discrete(("on", "off"), default="off"), Bool(default=True))
    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_two(self, req, i, d, b):
        return ("ok", i, d, b)

    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    @request(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_three(self, req, i, d, b):
        return ("ok", i, d, b)

    @return_reply()
    @request()
    def request_four(self, req):
        return ["ok"]

    @inform(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def inform_one(self, i, d, b):
        pass

    @request(Timestamp(), Timestamp(optional=True), major=4)
    @return_reply(Timestamp(), Timestamp(default=321), major=4)
    def request_katcpv4_time(self, req, timestamp1, timestamp2):
        self.katcpv4_time1 = timestamp1
        self.katcpv4_time2 = timestamp2
        if timestamp2:
            return ('ok', timestamp1, timestamp2)
        else:
            return ('ok', timestamp1)

    @request(Timestamp(multiple=True), major=4)
    @return_reply(Timestamp(multiple=True), major=4)
    def request_katcpv4_time_multi(self, req, *timestamps):
        self.katcpv4_time_multi = timestamps
        return ('ok', ) + timestamps

    @return_reply(Int(), Str())
    @request(Int(), include_msg=True)
    def request_eight(self, req, msg, i):
        return ("ok", i, msg.name)

    @request(Int(), Float(multiple=True))
    @return_reply(Int(), Float(multiple=True))
    def request_int_multifloat(self, req, i, *floats):
        return ('ok', i) + floats
class FbfWorkerServer(AsyncDeviceServer):
    VERSION_INFO = ("fbf-control-server-api", 0, 1)
    BUILD_INFO = ("fbf-control-server-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]
    STATES = [
        "idle", "preparing", "ready", "starting", "capturing", "stopping",
        "error"
    ]
    IDLE, PREPARING, READY, STARTING, CAPTURING, STOPPING, ERROR = STATES

    def __init__(self, ip, port, capture_interface, numa_node, exec_mode=FULL):
        """
        @brief       Construct new FbfWorkerServer instance

        @params  ip       The interface address on which the server should listen
        @params  port     The port that the server should bind to
        @params  de_ip    The IP address of the delay engine server
        @params  de_port  The port number for the delay engine server

        """
        self._dc_ip = None
        self._dc_port = None
        self._delay_client = None
        self._delays = None
        self._numa = numa_node
        self._exec_mode = exec_mode
        self._dada_input_key = "dada"
        self._dada_coh_output_key = "caca"
        self._dada_incoh_output_key = "baba"
        self._capture_interface = capture_interface
        self._capture_monitor = None

        self._input_level = 10.0
        self._output_level = 10.0
        self._partition_bandwidth = None
        self._centre_frequency = None

        super(FbfWorkerServer, self).__init__(ip, port)

    @coroutine
    def start(self):
        """Start FbfWorkerServer server"""
        super(FbfWorkerServer, self).start()

    @coroutine
    def stop(self):
        yield super(FbfWorkerServer, self).stop()

    def setup_sensors(self):
        """
        @brief    Set up monitoring sensors.

        Sensor list:
        - device-status
        - local-time-synced
        - fbf0-status
        - fbf1-status

        @note     The following sensors are made available on top of default
                  sensors implemented in AsynDeviceServer and its base classes.

                  device-status:      Reports the health status of the FBFUSE
                                      and associated devices:
                                      Among other things report HW failure, SW
                                      failure and observation failure.
        """
        self._device_status_sensor = Sensor.discrete(
            "device-status",
            description="Health status of FbfWorkerServer instance",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._device_status_sensor)

        self._state_sensor = LoggingSensor.discrete(
            "state",
            params=self.STATES,
            description="The current state of this worker instance",
            default=self.IDLE,
            initial_status=Sensor.NOMINAL)
        self._state_sensor.set_logger(log)
        self.add_sensor(self._state_sensor)

        self._capture_interface_sensor = Sensor.string(
            "capture-interface",
            description="The IP address of the NIC to be used for data capture",
            default=self._capture_interface,
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._capture_interface_sensor)

        self._delay_client_sensor = Sensor.string(
            "delay-engine-server",
            description="The address of the currently set delay engine",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._delay_client_sensor)

        self._antenna_capture_order_sensor = Sensor.string(
            "antenna-capture-order",
            description=
            "The order in which the worker will capture antennas internally",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._antenna_capture_order_sensor)

        self._mkrecv_header_sensor = Sensor.string(
            "mkrecv-capture-header",
            description=
            "The MKRECV/DADA header used for configuring capture with MKRECV",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._mkrecv_header_sensor)

        self._mksend_coh_header_sensor = Sensor.string(
            "mksend-coherent-beam-header",
            description=
            "The MKSEND/DADA header used for configuring transmission of coherent beam data",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._mksend_coh_header_sensor)

        self._mksend_incoh_header_sensor = Sensor.string(
            "mksend-incoherent-beam-header",
            description=
            "The MKSEND/DADA header used for configuring transmission of incoherent beam data",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._mksend_incoh_header_sensor)

        self._psrdada_cpp_args_sensor = Sensor.string(
            "psrdada-cpp-arguments",
            description="The command line arguments used to invoke psrdada_cpp",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._psrdada_cpp_args_sensor)

        self._mkrecv_heap_loss = Sensor.float(
            "feng-heap-loss",
            description=("The percentage if F-engine heaps lost "
                         "(within MKRECV statistics window)"),
            default=0.0,
            initial_status=Sensor.UNKNOWN,
            unit="%")
        self.add_sensor(self._mkrecv_heap_loss)

        self._ingress_buffer_percentage = Sensor.float(
            "ingress-buffer-fill-level",
            description=("The percentage fill level for the capture"
                         "buffer between MKRECV and PSRDADA_CPP"),
            default=0.0,
            initial_status=Sensor.UNKNOWN,
            unit="%")
        self.add_sensor(self._ingress_buffer_percentage)

        self._cb_egress_buffer_percentage = Sensor.float(
            "cb-egress-buffer-fill-level",
            description=("The percentage fill level for the transmission"
                         "buffer between PSRDADA_CPP and MKSEND (for "
                         "coherent beams)"),
            default=0.0,
            initial_status=Sensor.UNKNOWN,
            unit="%")
        self.add_sensor(self._cb_egress_buffer_percentage)

        self._ib_egress_buffer_percentage = Sensor.float(
            "ib-egress-buffer-fill-level",
            description=("The percentage fill level for the transmission"
                         "buffer between PSRDADA_CPP and MKSEND (for "
                         "incoherent beams)"),
            default=0.0,
            initial_status=Sensor.UNKNOWN,
            unit="%")
        self.add_sensor(self._ib_egress_buffer_percentage)

    @property
    def capturing(self):
        return self.state == self.CAPTURING

    @property
    def idle(self):
        return self.state == self.IDLE

    @property
    def starting(self):
        return self.state == self.STARTING

    @property
    def stopping(self):
        return self.state == self.STOPPING

    @property
    def ready(self):
        return self.state == self.READY

    @property
    def preparing(self):
        return self.state == self.PREPARING

    @property
    def error(self):
        return self.state == self.ERROR

    @property
    def state(self):
        return self._state_sensor.value()

    def _system_call_wrapper(self, cmd):
        log.debug("System call: '{}'".format(" ".join(cmd)))
        check_call(cmd)

    @coroutine
    def _make_db(self, key, block_size, nblocks, timeout=120):
        try:
            yield self._destroy_db(key, timeout=20)
        except Exception as error:
            log.debug("Could not clean previous buffer (key={}): {}".format(
                key, str(error)))
        log.debug(("Building DADA buffer: key={}, block_size={}, "
                   "nblocks={}").format(key, block_size, nblocks))
        if self._exec_mode == FULL:
            cmdline = map(str, [
                "dada_db", "-k", key, "-b", block_size, "-n", nblocks, "-l",
                "-p"
            ])
            proc = Popen(cmdline,
                         stdout=PIPE,
                         stderr=PIPE,
                         shell=False,
                         close_fds=True)
            yield process_watcher(proc,
                                  name="make_db({})".format(key),
                                  timeout=timeout)
        else:
            log.warning(("Current execution mode disables "
                         "DADA buffer creation/destruction"))

    @coroutine
    def _destroy_db(self, key, timeout=20.0):
        log.debug("Destroying DADA buffer with key={}".format(key))
        if self._exec_mode == FULL:
            cmdline = map(str, ["dada_db", "-k", key, "-d"])
            proc = Popen(cmdline,
                         stdout=PIPE,
                         stderr=PIPE,
                         shell=False,
                         close_fds=True)
            yield process_watcher(proc,
                                  name="destroy_db({})".format(key),
                                  timeout=timeout)
        else:
            log.warning(("Current execution mode disables "
                         "DADA buffer creation/destruction"))

    @coroutine
    def _reset_db(self, key, timeout=5.0):
        log.debug("Resetting DADA buffer with key={}".format(key))
        if self._exec_mode == FULL:
            cmdline = map(str, ["dbreset", "-k", key])
            proc = Popen(cmdline,
                         stdout=PIPE,
                         stderr=PIPE,
                         shell=False,
                         close_fds=True)
            yield process_watcher(proc,
                                  name="reset_db({})".format(key),
                                  timeout=timeout)
        else:
            log.warning(("Current execution mode disables "
                         "DADA buffer reset"))

    def set_affinity(self, pid, core_spec):
        log.debug("Setting affinity for PID {} to {}".format(pid, core_spec))
        os.system("taskset -cp {} {}".format(core_spec, pid))

    @request(Float(), Float())
    @return_reply()
    def request_set_levels(self, req, input_level, output_level):
        """
        @brief    Set the input and output levels for FBFUSE

        @param      req             A katcp request object

        @param    input_level  The standard deviation of the data
                               from the F-engines.

        @param    output_level  The standard deviation of the data
                                output from FBFUSE.
        """
        self._input_level = input_level
        self._output_level = output_level
        return ("ok", )

    @request(Str(), Int(), Int(), Float(), Float(), Str(), Str(), Str(), Str(),
             Int())
    @return_reply()
    def request_prepare(self, req, feng_groups, nchans_per_group, chan0_idx,
                        chan0_freq, chan_bw, feng_config, coherent_beam_config,
                        incoherent_beam_config, dc_ip, dc_port):
        """
        @brief      Prepare FBFUSE to receive and process data from a subarray

        @detail     REQUEST ?configure feng_groups, nchans_per_group, chan0_idx, chan0_freq,
                        chan_bw, mcast_to_beam_map, antenna_to_feng_id_map, coherent_beam_config,
                        incoherent_beam_config
                    Configure FBFUSE for the particular data products

        @param      req                 A katcp request object

        @param      feng_groups         The contiguous range of multicast groups to capture F-engine data from,
                                        the parameter is formatted in stream notation, e.g.: spead://239.11.1.150+3:7148

        @param      nchans_per_group    The number of frequency channels per multicast group

        @param      chan0_idx           The index of the first channel in the set of multicast groups

        @param      chan0_freq          The frequency in Hz of the first channel in the set of multicast groups

        @param      chan_bw             The channel bandwidth in Hz

        @param      feng_config    JSON dictionary containing general F-engine parameters.

                                        @code
                                           {
                                              'bandwidth': 856e6,
                                              'centre-frequency': 1200e6,
                                              'sideband': 'upper',
                                              'feng-antenna-map': {...},
                                              'sync-epoch': 12353524243.0,
                                              'nchans': 4096
                                           }

        @param      coherent_beam_config   A JSON object specifying the coherent beam configuration in the form:

                                           @code
                                              {
                                                'tscrunch':16,
                                                'fscrunch':1,
                                                'nbeams': 400,
                                                'antennas':'m007,m008,m009',
                                                'destination': 'spead://239.11.1.0+127:7148'
                                              }
                                           @endcode

        @param      incoherent_beam_config  A JSON object specifying the incoherent beam configuration in the form:

                                           @code
                                              {
                                                'tscrunch':16,
                                                'fscrunch':1,
                                                'antennas':'m007,m008,m009',
                                                'destination': 'spead://239.11.1.150:7148'
                                              }
                                           @endcode

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """
        if not self.idle:
            return ("fail", "FBF worker not in IDLE state")

        log.info("Preparing worker server instance")
        try:
            feng_config = json.loads(feng_config)
        except Exception as error:
            msg = ("Unable to parse F-eng config with "
                   "error: {}").format(str(error))
            log.error("Prepare failed: {}".format(msg))
            return ("fail", msg)
        log.info("F-eng config: {}".format(feng_config))
        try:
            coherent_beam_config = json.loads(coherent_beam_config)
        except Exception as error:
            msg = ("Unable to parse coherent beam "
                   "config with error: {}").format(str(error))
            log.error("Prepare failed: {}".format(msg))
            return ("fail", msg)
        log.info("Coherent beam config: {}".format(coherent_beam_config))
        try:
            incoherent_beam_config = json.loads(incoherent_beam_config)
        except Exception as error:
            msg = ("Unable to parse incoherent beam "
                   "config with error: {}").format(str(error))
            log.error("Prepare failed: {}".format(msg))
            return ("fail", msg)
        log.info("Incoherent beam config: {}".format(incoherent_beam_config))

        @coroutine
        def configure():
            self._state_sensor.set_value(self.PREPARING)
            log.debug("Starting delay configuration server client")
            self._delay_client = KATCPClientResource(
                dict(name="delay-configuration-client",
                     address=(dc_ip, dc_port),
                     controlled=True))
            self._delay_client.start()

            log.info("Determining F-engine capture order")
            feng_capture_order_info = determine_feng_capture_order(
                feng_config['feng-antenna-map'], coherent_beam_config,
                incoherent_beam_config)
            log.info("F-engine capture order info: {}".format(
                feng_capture_order_info))
            feng_to_antenna_map = {
                value: key
                for key, value in feng_config['feng-antenna-map'].items()
            }
            antenna_capture_order_csv = ",".join([
                feng_to_antenna_map[feng_id]
                for feng_id in feng_capture_order_info['order']
            ])
            self._antenna_capture_order_sensor.set_value(
                antenna_capture_order_csv)

            log.debug("Parsing F-engines to capture: {}".format(feng_groups))
            capture_range = ip_range_from_stream(feng_groups)
            ngroups = capture_range.count
            partition_nchans = nchans_per_group * ngroups
            worker_idx = chan0_idx / partition_nchans
            partition_bandwidth = partition_nchans * chan_bw
            self._partition_bandwidth = partition_bandwidth
            sample_clock = feng_config['bandwidth'] * 2
            timestamp_step = feng_config['nchans'] * 2 * 256
            frequency_ids = [
                chan0_idx + nchans_per_group * ii for ii in range(ngroups)
            ]
            nantennas = len(feng_capture_order_info['order'])
            heap_size = nchans_per_group * PACKET_PAYLOAD_SIZE
            heap_group_size = ngroups * heap_size * nantennas
            ngroups_data = int(MAX_DADA_BLOCK_SIZE / heap_group_size)
            ngroups_data = 2**((ngroups_data - 1).bit_length())
            centre_frequency = chan0_freq + self._partition_bandwidth / 2.0
            self._centre_frequency = centre_frequency

            # Coherent beam timestamps
            coh_heap_size = 8192
            nsamps_per_coh_heap = (
                coh_heap_size /
                (partition_nchans * coherent_beam_config['fscrunch']))
            coh_timestamp_step = (coherent_beam_config['tscrunch'] *
                                  nsamps_per_coh_heap * 2 *
                                  feng_config["nchans"])

            # Incoherent beam timestamps
            incoh_heap_size = 8192
            nsamps_per_incoh_heap = (
                incoh_heap_size /
                (partition_nchans * incoherent_beam_config['fscrunch']))
            incoh_timestamp_step = (incoherent_beam_config['tscrunch'] *
                                    nsamps_per_incoh_heap * 2 *
                                    feng_config["nchans"])

            timestamp_modulus = lcm(
                timestamp_step, lcm(incoh_timestamp_step, coh_timestamp_step))

            if self._exec_mode == FULL:
                dada_mode = 4
            else:
                dada_mode = 0
            mkrecv_config = {
                'dada_mode':
                dada_mode,
                'dada_key':
                self._dada_input_key,
                'sync_epoch':
                feng_config['sync-epoch'],
                'sample_clock':
                sample_clock,
                'mcast_sources':
                ",".join([str(group) for group in capture_range]),
                'mcast_port':
                capture_range.port,
                'interface':
                self._capture_interface,
                'timestamp_step':
                timestamp_step,
                'timestamp_modulus':
                timestamp_modulus,
                'ordered_feng_ids_csv':
                ",".join(map(str, feng_capture_order_info['order'])),
                'frequency_partition_ids_csv':
                ",".join(map(str, frequency_ids)),
                'ngroups_data':
                ngroups_data,
                'heap_size':
                heap_size
            }
            mkrecv_header = make_mkrecv_header(mkrecv_config,
                                               outfile=MKRECV_CONFIG_FILENAME)
            self._mkrecv_header_sensor.set_value(mkrecv_header)
            log.info(
                "Determined MKRECV configuration:\n{}".format(mkrecv_header))

            coh_ip_range = ip_range_from_stream(
                coherent_beam_config['destination'])
            nbeams = coherent_beam_config['nbeams']
            nbeams_per_group = nbeams / coh_ip_range.count
            msg = "nbeams is not a mutliple of the IP range"
            assert nbeams % coh_ip_range.count == 0, msg
            """
            Note on data rates:
            For both the coherent and incoherent beams, we set the sending
            rate in MKSEND equal to 110% of the required data rate. This
            is a fudge to ensure that we send rapidly enough that MKSEND
            does not limit performance while at the same time ensuring that
            the burst rate out of the instrument is limited. This number
            may need to be tuned.
            """
            coh_data_rate = (partition_bandwidth /
                             coherent_beam_config['tscrunch'] /
                             coherent_beam_config['fscrunch'] *
                             nbeams_per_group * 1.1)
            heap_id_start = worker_idx * coh_ip_range.count
            log.debug("Determining MKSEND configuration for coherent beams")
            dada_mode = int(self._exec_mode == FULL)
            coherent_mcast_dest = coherent_beam_config['destination'].lstrip(
                "spead://").split(":")[0]
            mksend_coh_config = {
                'dada_key': self._dada_coh_output_key,
                'dada_mode': dada_mode,
                'interface': self._capture_interface,
                'data_rate': coh_data_rate,
                'mcast_port': coh_ip_range.port,
                'mcast_destinations': coherent_mcast_dest,
                'sync_epoch': feng_config['sync-epoch'],
                'sample_clock': sample_clock,
                'heap_size': coh_heap_size,
                'heap_id_start': heap_id_start,
                'timestamp_step': coh_timestamp_step,
                'beam_ids': "0:{}".format(nbeams),
                'multibeam': True,
                'subband_idx': chan0_idx,
                'heap_group': nbeams_per_group
            }
            mksend_coh_header = make_mksend_header(
                mksend_coh_config, outfile=MKSEND_COHERENT_CONFIG_FILENAME)
            log.info(("Determined MKSEND configuration for coherent beams:\n{}"
                      ).format(mksend_coh_header))
            self._mksend_coh_header_sensor.set_value(mksend_coh_header)

            log.debug("Determining MKSEND configuration for incoherent beams")
            incoh_data_rate = (partition_bandwidth /
                               incoherent_beam_config['tscrunch'] /
                               incoherent_beam_config['fscrunch'] * 1.1)
            dada_mode = int(self._exec_mode == FULL)
            incoh_ip_range = ip_range_from_stream(
                incoherent_beam_config['destination'])
            coherent_mcast_dest = incoherent_beam_config['destination'].lstrip(
                "spead://").split(":")[0]
            mksend_incoh_config = {
                'dada_key': self._dada_incoh_output_key,
                'dada_mode': dada_mode,
                'interface': self._capture_interface,
                'data_rate': incoh_data_rate,
                'mcast_port': incoh_ip_range.port,
                'mcast_destinations': coherent_mcast_dest,
                'sync_epoch': feng_config['sync-epoch'],
                'sample_clock': sample_clock,
                'heap_size': incoh_heap_size,
                'heap_id_start': worker_idx,
                'timestamp_step': incoh_timestamp_step,
                'beam_ids': 0,
                'multibeam': False,
                'subband_idx': chan0_idx,
                'heap_group': 1
            }
            mksend_incoh_header = make_mksend_header(
                mksend_incoh_config, outfile=MKSEND_INCOHERENT_CONFIG_FILENAME)
            log.info(
                "Determined MKSEND configuration for incoherent beam:\n{}".
                format(mksend_incoh_header))
            self._mksend_incoh_header_sensor.set_value(mksend_incoh_header)
            """
            Tasks:
                - compile kernels
                - create shared memory banks
            """
            # Here we create a future object for the psrdada_cpp compilation
            # this is the longest running setup task and so intermediate steps
            # such as dada buffer generation
            fbfuse_pipeline_params = {
                'total_nantennas':
                len(feng_capture_order_info['order']),
                'fbfuse_nchans':
                partition_nchans,
                'total_nchans':
                feng_config['nchans'],
                'coherent_tscrunch':
                coherent_beam_config['tscrunch'],
                'coherent_fscrunch':
                coherent_beam_config['fscrunch'],
                'coherent_nantennas':
                len(coherent_beam_config['antennas'].split(",")),
                'coherent_antenna_offset':
                feng_capture_order_info["coherent_span"][0],
                'coherent_nbeams':
                nbeams,
                'incoherent_tscrunch':
                incoherent_beam_config['tscrunch'],
                'incoherent_fscrunch':
                incoherent_beam_config['fscrunch']
            }
            psrdada_compilation_future = compile_psrdada_cpp(
                fbfuse_pipeline_params)

            log.info("Creating all DADA buffers")
            # Create capture data DADA buffer
            capture_block_size = ngroups_data * heap_group_size
            capture_block_count = int(AVAILABLE_CAPTURE_MEMORY /
                                      capture_block_size)
            log.debug("Creating dada buffer for input with key '{}'".format(
                "%s" % self._dada_input_key))
            input_make_db_future = self._make_db(self._dada_input_key,
                                                 capture_block_size,
                                                 capture_block_count)

            # Create coherent beam output DADA buffer
            coh_output_channels = (ngroups * nchans_per_group) / \
                coherent_beam_config['fscrunch']
            coh_output_samples = ngroups_data * \
                256 / coherent_beam_config['tscrunch']
            coherent_block_size = (nbeams * coh_output_channels *
                                   coh_output_samples)
            coherent_block_count = 32
            log.debug(
                ("Creating dada buffer for coherent beam output "
                 "with key '{}'").format("%s" % self._dada_coh_output_key))
            coh_output_make_db_future = self._make_db(
                self._dada_coh_output_key, coherent_block_size,
                coherent_block_count)

            # Create incoherent beam output DADA buffer
            incoh_output_channels = ((ngroups * nchans_per_group) /
                                     incoherent_beam_config['fscrunch'])
            incoh_output_samples = ((ngroups_data * 256) /
                                    incoherent_beam_config['tscrunch'])
            incoherent_block_size = incoh_output_channels * incoh_output_samples
            incoherent_block_count = 32
            log.debug(("Creating dada buffer for incoherent beam "
                       "output with key '{}'").format(
                           "%s" % self._dada_incoh_output_key))
            incoh_output_make_db_future = self._make_db(
                self._dada_incoh_output_key, incoherent_block_size,
                incoherent_block_count)

            # Need to pass the delay buffer controller the F-engine capture
            # order but only for the coherent beams
            cstart, cend = feng_capture_order_info['coherent_span']
            coherent_beam_feng_capture_order = feng_capture_order_info[
                'order'][cstart:cend]
            coherent_beam_antenna_capture_order = [
                feng_to_antenna_map[idx]
                for idx in coherent_beam_feng_capture_order
            ]

            # Start DelayBufferController instance
            # Here we are going to make the assumption that the server and processing all run in
            # one docker container that will be preallocated with the right CPU set, GPUs, memory
            # etc. This means that the configurations need to be unique by NUMA node... [Note: no
            # they don't, we can use the container IPC channel which isolates
            # the IPC namespaces.]
            #
            # Here we recreate the beam keys as they are handled by the BeamManager
            # instance in the product controller
            #
            beam_idxs = ["cfbf%05d" % (i) for i in range(nbeams)]
            self._delay_buf_ctrl = DelayBufferController(
                self._delay_client, beam_idxs,
                coherent_beam_antenna_capture_order, 1)
            yield self._delay_buf_ctrl.start()

            # By this point we require psrdada_cpp to have been compiled
            # as such we can yield on the future we created earlier
            yield psrdada_compilation_future

            # Now we can yield on dada buffer generation
            yield input_make_db_future
            yield coh_output_make_db_future
            yield incoh_output_make_db_future
            self._state_sensor.set_value(self.READY)
            log.info("Prepare request successful")
            req.reply("ok", )

        @coroutine
        def safe_configure():
            try:
                yield configure()
            except Exception as error:
                log.exception(str(error))
                req.reply("fail", str(error))

        self.ioloop.add_callback(safe_configure)
        raise AsyncReply

    @request()
    @return_reply()
    def request_deconfigure(self, req):
        """
        @brief      Deconfigure the FBFUSE instance.

        @note       Deconfigure the FBFUSE instance. If FBFUSE uses katportalclient to get information
                    from CAM, then it should disconnect at this time.

        @param      req               A katcp request object

        @return     katcp reply object [[[ !deconfigure ok | (fail [error description]) ]]]
        """

        # Need to make sure everything is stopped
        # Call self.stop?

        # Need to delete all allocated DADA buffers:
        log.info("Received deconfigure request")

        @coroutine
        def deconfigure():
            log.info("Destroying allocated DADA buffers")
            try:
                yield self._destroy_db(self._dada_input_key)
                yield self._destroy_db(self._dada_coh_output_key)
                yield self._destroy_db(self._dada_incoh_output_key)
            except Exception as error:
                log.warning("Error while destroying DADA buffers: {}".format(
                    str(error)))
            log.info("Destroying delay buffers")
            del self._delay_buf_ctrl
            self._delay_buf_ctrl = None
            self._state_sensor.set_value(self.IDLE)
            log.info("Deconfigure request successful")
            req.reply("ok", )

        self.ioloop.add_callback(deconfigure)
        raise AsyncReply

    @request()
    @return_reply()
    def request_capture_start(self, req):
        """
        @brief      Prepare FBFUSE ingest process for data capture.

        @note       A successful return value indicates that FBFUSE is ready for data capture and
                    has sufficient resources available. An error will indicate that FBFUSE is not
                    in a position to accept data

        @param      req               A katcp request object


        @return     katcp reply object [[[ !capture-init ok | (fail [error description]) ]]]
        """
        log.info("Received capture-start request")
        try:
            self.capture_start()
        except Exception as error:
            log.exception("Error during capture start")
            return ("fail", str(error))
        else:
            log.info("Capture-start successful")
            return ("ok", )

    def capture_start(self):
        if not self.ready:
            raise Exception("FBF worker not in READY state")
        self._state_sensor.set_value(self.STARTING)
        # Create SPEAD transmitter for coherent beams

        if self._numa == 0:
            mksend_cpu_set = "7"
            psrdada_cpp_cpu_set = "6"
            mkrecv_cpu_set = "0-5"
        else:
            mksend_cpu_set = "14"
            psrdada_cpp_cpu_set = "15"
            mkrecv_cpu_set = "8-13"

        self._mksend_coh_proc = ManagedProcess([
            "taskset", "-c", mksend_cpu_set, "mksend", "--header",
            MKSEND_COHERENT_CONFIG_FILENAME, "--quiet"
        ])

        self._mksend_incoh_proc = ManagedProcess([
            "taskset", "-c", mksend_cpu_set, "mksend", "--header",
            MKSEND_INCOHERENT_CONFIG_FILENAME, "--quiet"
        ])

        # Start beamforming pipeline
        log.info("Starting PSRDADA_CPP beamforming pipeline")
        delay_buffer_key = self._delay_buf_ctrl.shared_buffer_key
        # Start beamformer instance
        psrdada_cpp_cmdline = [
            "taskset", "-c", psrdada_cpp_cpu_set, "fbfuse", "--input_key",
            self._dada_input_key, "--cb_key", self._dada_coh_output_key,
            "--ib_key", self._dada_incoh_output_key, "--delay_key_root",
            delay_buffer_key, "--cfreq", self._centre_frequency, "--bandwidth",
            self._partition_bandwidth, "--input_level", self._input_level,
            "--output_level", self._output_level, "--log_level", "info"
        ]
        self._psrdada_cpp_args_sensor.set_value(" ".join(
            map(str, psrdada_cpp_cmdline)))
        log.debug(" ".join(map(str, psrdada_cpp_cmdline)))
        self._psrdada_cpp_proc = ManagedProcess(psrdada_cpp_cmdline)

        def update_heap_loss_sensor(curr, total, avg, window):
            self._mkrecv_heap_loss.set_value(100.0 - avg)

        # Create SPEAD receiver for incoming antenna voltages
        self._mkrecv_proc = ManagedProcess(
            [
                "taskset", "-c", mkrecv_cpu_set, "mkrecv_nt", "--header",
                MKRECV_CONFIG_FILENAME, "--quiet"
            ],
            stdout_handler=MkrecvStdoutHandler(
                callback=update_heap_loss_sensor))

        def exit_check_callback():
            if not self._mkrecv_proc.is_alive():
                log.error("mkrecv_nt exited unexpectedly")
                self.ioloop.add_callback(self.capture_stop)
            if not self._psrdada_cpp_proc.is_alive():
                log.error("fbfuse pipeline exited unexpectedly")
                self.ioloop.add_callback(self.capture_stop)
            if not self._mksend_coh_proc.is_alive():
                log.error("mksend coherent exited unexpectedly")
                self.ioloop.add_callback(self.capture_stop)
            if not self._mksend_incoh_proc.is_alive():
                log.error("mksend incoherent exited unexpectedly")
                self.ioloop.add_callback(self.capture_stop)
            self._capture_monitor.stop()

        self._capture_monitor = PeriodicCallback(exit_check_callback, 1000)
        self._capture_monitor.start()

        def dada_callback(params):
            self._ingress_buffer_percentage.set_value(params["fraction-full"])

        # start DB monitors
        self._ingress_buffer_monitor = DbMonitor(self._dada_input_key,
                                                 callback=dada_callback)
        self._ingress_buffer_monitor.start()
        self._cb_egress_buffer_monitor = DbMonitor(
            self._dada_input_key,
            callback=lambda params: self._cb_egress_buffer_percentage.
            set_value(params["fraction-full"]))
        self._cb_egress_buffer_monitor.start()
        self._ib_egress_buffer_monitor = DbMonitor(
            self._dada_input_key,
            callback=lambda params: self._ib_egress_buffer_percentage.
            set_value(params["fraction-full"]))
        self._ib_egress_buffer_monitor.start()
        self._state_sensor.set_value(self.CAPTURING)

    @request()
    @return_reply()
    def request_capture_stop(self, req):
        """
        @brief      Terminate the FBFUSE ingest process for the particular FBFUSE instance

        @note       This writes out any remaining metadata, closes all files, terminates any remaining processes and
                    frees resources for the next data capture.

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, used to track which subarray is being told to stop capture.
                                      For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !capture-done ok | (fail [error description]) ]]]
        """
        log.info("Received capture-stop request")

        @coroutine
        def capture_stop_wrapper():
            try:
                yield self.capture_stop()
            except Exception as error:
                log.exception("Capture-stop request failed")
                req.reply("fail", str(error))
            else:
                log.info("Capture-stop request successful")
                req.reply("ok", )

        self.ioloop.add_callback(capture_stop_wrapper)
        raise AsyncReply

    @coroutine
    def capture_stop(self):
        if not self.capturing and not self.error:
            return
        log.info("Stopping capture")
        self._state_sensor.set_value(self.STOPPING)
        self._capture_monitor.stop()
        self._ingress_buffer_monitor.stop()
        self._cb_egress_buffer_monitor.stop()
        self._ib_egress_buffer_monitor.stop()
        log.info("Stopping MKRECV instance")
        self._mkrecv_proc.terminate()
        log.info("Stopping PSRDADA_CPP instance")
        self._psrdada_cpp_proc.terminate()
        log.info("Stopping MKSEND instances")
        self._mksend_incoh_proc.terminate()
        self._mksend_coh_proc.terminate()
        log.info("Resetting DADA buffers")
        reset_tasks = []
        reset_tasks.append(self._reset_db(self._dada_input_key, timeout=7.0))
        reset_tasks.append(
            self._reset_db(self._dada_coh_output_key, timeout=4.0))
        reset_tasks.append(
            self._reset_db(self._dada_incoh_output_key, timeout=5.0))
        for task in reset_tasks:
            try:
                yield task
            except Exception as error:
                log.warning("Error raised on DB reset: {}".format(str(error)))
        self._state_sensor.set_value(self.READY)
class BLBackendInterface(AsyncDeviceServer):
    """Breakthrough Listen's KATCP Server Backend Interface

    This server responds to requests sent from CAM, most notably:
        @ configue
        @ capture-init
        @ capture-start
        @ capture-stop
        @ capture-done
        @ deconfigure

    But because it inherits from AsyncDeviceServer, also responds to:
        * halt
        * help
        * log-level
        * restart [#restartf1]_
        * client-list
        * sensor-list
        * sensor-sampling
        * sensor-value
        * watchdog
        * version-list (only standard in KATCP v5 or later)
        * request-timeout-hint (pre-standard only if protocol flags indicates
                              timeout hints, supported for KATCP v5.1 or later)
        * sensor-sampling-clear (non-standard)
    """

    VERSION_INFO = ("BLUSE-katcp-interface", 1, 0)
    BUILD_INFO = ("BLUSE-katcp-implementation", 1, 0, "rc?")
    DEVICE_STATUSES = ["ok", "fail", "degraded"]

    def __init__(self, server_host, server_port):
        self.port = server_port
        self.redis_server = redis.StrictRedis()
        super(BLBackendInterface, self).__init__(server_host, server_port)

    def start(self):
        """Start the server

        Based on the passed configuration object this is
        where the clients for suboridnates nodes will be
        set up.
        """
        super(BLBackendInterface, self).start()
        print(R"""
                      ,'''''-._
                     ;  ,.  <> `-._
                     ;  \'   _,--'"
                    ;      (
                    ; ,   ` \
                    ;, ,     \
                   ;    |    |        MeerKAT BL Backend Interface:
                   ; |, |    |\       KATCP Server
                  ;  |  |    | \      Version: {}
                  |.-\ ,\    |\ :     Port: {}
                  |.| `. `-. | ||
                  :.|   `-. \ ';;
                   .- ,   \;;|
                   ;   ,  |  ,\
                   ; ,    ;    \      https://github.com/ejmichaud/meerkat-backend-interface
                  ;    , /`.  , )
               __,;,   ,'   \  ,|
         _,--''__,|   /      \  :
       ,'_,-''    | ,/        | :
      / /         | ;         ; |
     | |      __,-| |--..__,--| |---.--....___
___,-| |----''    / |         `._`-.          `----
      \ \        `'''             '''      --
       `.`.                 --'
         `.`-._        _,             ,-     __,-
            `-.`.
   --'         `;
        """.format("{}.{}".format(self.VERSION_INFO[1], self.VERSION_INFO[2]),
                   self.port))

    @request(Str(), Str(), Int(), Str(), Str())
    @return_reply()
    def request_configure(self, req, product_id, antennas_csv, n_channels,
                          streams_json, proxy_name):
        """Receive metadata for upcoming observation.

        In order to allow BLUSE to make an estimate of its ability
        to process a particular data product, this command should
        be used to configure a BLUSE instance when a new subarray is activated.

        Args:
            product_id (str): This is a name for the data product,
                    which is a useful tag to include in the data,
                    but should not be analysed further.
                    For example "array_1_bc856M4k". This value will
                    be unique across all subarrays. However, it is
                    not a globally unique identifier for the lifetime
                    of the telescope.  The exact same value may be provided
                    at a later time when the same subarray is activated again.

            antennas_csv (str): A comma separated list of physical antenna names
                    used in particular sub-array to which the data products belongs.

            n_channels (int): The integer number of frequency channels provided by the CBF.

            streams_json (str) is a JSON struct containing config keys and
                    values describing the streams.  For example:
                    {'stream_type1': {
                    'stream_name1': 'stream_address1',
                    'stream_name2': 'stream_address2',
                    ...},
                 'stream_type2': {
                    'stream_name1': 'stream_address1',
                    'stream_name2': 'stream_address2',
                    ...},
                    ...}
                The steam type keys indicate the source of the data and the type, e.g. cam.http.
                stream_address will be a URI.  For SPEAD streams, the format will be
                spead://<ip>[+<count>]:<port>, representing SPEAD stream multicast groups.
                When a single logical stream requires too much bandwidth to accommodate
                as a single multicast group, the count parameter indicates the number of
                additional consecutively numbered multicast group ip addresses, and
                sharing the same UDP port number.
                stream_name is the name used to identify the stream in CAM.
                A Python example is shown below, for five streams:
                One CAM stream, with type cam.http.  The camdata stream provides the
                connection string for katportalclient (for the subarray that this
                BLUSE instance is being configured on).
                One F-engine stream, with type:  cbf.antenna_channelised_voltage.
                One X-engine stream, with type:  cbf.baseline_correlation_products.
                Two beam streams, with type: cbf.tied_array_channelised_voltage.
                The stream names ending in x are horizontally polarised, and those
                ending in y are vertically polarised.

            proxy_name (str): The CAM name for the instance of the BLUSE data
                proxy that is being configured.  For example, "BLUSE_3".  This
                can be used to query sensors on the correct proxy.  Note that for
                BLUSE there will only be a single instance of the proxy in a subarray.

        Returns:
            None... but replies with "ok" or "fail" and logs either info or error

        Writes:
            - subbarry1_abc65555:timestamp" -> "1534657577373.23423"  :: Redis String
            - subarray1_abc65555:antennas" -> [1,2,3,4] :: Redis List
            - subarray1_abc65555:n_channels" -> "4096" :: Redis String
            - subarray1_abc65555:proxy_name "-> "BLUSE_whatever" :: Redis String
            - subarray1_abc65555:streams" -> {....} :: Redis Hash !!!CURRENTLY A STRING!!!
            - current:obs:id -> "subbary1_abc65555"

        Publishes:
            redis-channel: 'alerts' <-- "configure"

        Examples:
            > ?configure array_1_bc856M4k a1,a2,a3,a4 128000 {"cam.http":{"camdata":"http://monctl.devnmk.camlab.kat.ac.za/api/client/2"},"stream_type2":{"stream_name1":"stream_address1","stream_name2":"stream_address2"}} BLUSE_3
        """
        try:
            antennas_list = antennas_csv.split(",")
            json_dict = unpack_dict(streams_json)
            cam_url = json_dict['cam.http']['camdata']
        except Exception as e:
            log.error(e)
            return ("fail", e)
        statuses = []
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:timestamp".format(product_id), time.time()))
        statuses.append(
            write_list_redis(self.redis_server,
                             "{}:antennas".format(product_id), antennas_list))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:n_channels".format(product_id), n_channels))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:proxy_name".format(product_id), proxy_name))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:streams".format(product_id),
                             json.dumps(json_dict)))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:cam:url".format(product_id), cam_url))
        statuses.append(
            write_pair_redis(self.redis_server, "current:obs:id", product_id))
        msg = "configure:{}".format(product_id)
        statuses.append(
            publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts, msg))
        if all(statuses):
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_init(self, req, product_id):
        """Signals that an observation will start soon

            Publishes a message to the 'alerts' channel of the form:
                capture-init:product_id
            The product_id should match what what was sent in the ?configure request

            This alert should notify all backend processes (such as beamformer)
            to get ready for data
        """
        msg = "capture-init:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_start(self, req, product_id):
        """Signals that an observation is starting now

            Publishes a message to the 'alerts' channel of the form:
                capture-start:product_id
            The product_id should match what what was sent in the ?configure request

            This alert should notify all backend processes (such as beamformer)
            that they need to be collecting data now
        """
        msg = "capture-start:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_stop(self, req, product_id):
        """Signals that an observation is has stopped

            Publishes a message to the 'alerts' channel of the form:
                capture-stop:product_id
            The product_id should match what what was sent in the ?configure request

            This alert should notify all backend processes (such as beamformer)
            that they should stop collecting data now
        """
        msg = "capture-stop:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_done(self, req, product_id):
        """Signals that an observation has finished

            Publishes a message to the 'alerts' channel of the form:
                capture-done:product_id
            The product_id should match what what was sent in the ?configure request

            This alert should notify all backend processes (such as beamformer)
            that their data streams are ending
        """

        msg = "capture-done:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_deconfigure(self, req, product_id):
        """Signals that the current data product is done.

            Deconfigure the BLUSE instance that was created by the call
            to ?configure with the corresponding product_id. Note:  CAM is
            expected to have sent a ?capture-done request before deconfiguring,
            in order to ensure that all data has been written. If BLUSE uses an
            instance of katportalclient to get information from CAM for this
            BLUSE instance, then it should disconnect at this time.

            Publishes a message to the 'alerts' channel of the form:
                deconfigure:product_id
            The product_id should match what what was sent in the ?configure request

            This alert should notify all backend processes (such as beamformer)
            that their data streams are ending
        """
        msg = "deconfigure:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    def setup_sensors(self):
        """
        @brief    Set up monitoring sensors.

        @note     The following sensors are made available on top of defaul sensors
                  implemented in AsynDeviceServer and its base classes.

                  device-status:      Reports the health status of the FBFUSE and associated devices:
                                      Among other things report HW failure, SW failure and observation failure.
        """
        self._device_status = Sensor.discrete(
            "device-status",
            description="Health status of BLUSE",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._device_status)

        self._local_time_synced = Sensor.boolean(
            "local-time-synced",
            description="Indicates BLUSE is NTP syncronised.",
            default=True,  # TODO: implement actual NTP synchronization request
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._local_time_synced)

        self._version = Sensor.string(
            "version",
            description="Reports the current BLUSE version",
            default=str(self.VERSION_INFO[1:]).strip('()').replace(
                ' ', '').replace(",", '.'),  # e.g. '1.0'
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._version)

    def request_halt(self, req, msg):
        """Halts the server, logs to syslog and slack, and exits the program
        Returns
        -------
        success : {'ok', 'fail'}
            Whether scheduling the halt succeeded.
        Examples
        --------
        ::
            ?halt
            !halt ok

        TODO:
            - Call halt method on superclass to avoid copy paste
                Doing this caused an issue:
                    File "/Users/Eric/Berkeley/seti/packages/meerkat/lib/python2.7/site-packages/katcp/server.py", line 1102, in handle_request
                        assert (reply.mtype == Message.REPLY)
                    AttributeError: 'NoneType' object has no attribute 'mtype'
        """
        f = Future()

        @gen.coroutine
        def _halt():
            req.reply("ok")
            yield gen.moment
            self.stop(timeout=None)
            raise AsyncReply

        self.ioloop.add_callback(lambda: chain_future(_halt(), f))
        log.critical("HALTING SERVER!!!")
        # TODO: uncomment when you deploy
        # notify_slack("KATCP server at MeerKAT has halted. Might want to check that!")
        sys.exit(0)

    @request()
    @return_reply(Str())
    def request_find_alien(self, req):
        """Finds an alien.
        """
        return ("ok", R"""
.     .       .  .   . .   .   . .    +  .
  .     .  :     .    .. :. .___---------___.
       .  .   .    .  :.:. _".^ .^ ^.  '.. :"-_. .
    .  :       .  .  .:../:            . .^  :.:\.
        .   . :: +. :.:/: .   .    .        . . .:\
 .  :    .     . _ :::/:               .  ^ .  . .:\
  .. . .   . - : :.:./.                        .  .:\
  .      .     . :..|:                    .  .  ^. .:|
    .       . : : ..||        .                . . !:|
  .     . . . ::. ::\(                           . :)/
 .   .     : . : .:.|. ######              .#######::|
  :.. .  :-  : .:  ::|.#######           ..########:|
 .  .  .  ..  .  .. :\ ########          :######## :/
  .        .+ :: : -.:\ ########       . ########.:/
    .  .+   . . . . :.:\. #######       #######..:/
      :: . . . . ::.:..:.\           .   .   ..:/
   .   .   .  .. :  -::::.\.       | |     . .:/
      .  :  .  .  .-:.":.::.\             ..:/
 .      -.   . . . .: .:::.:.\.           .:/
.   .   .  :      : ....::_:..:\   ___.  :/
   .   .  .   .:. .. .  .: :.:.:\       :/
     +   .   .   : . ::. :.:. .:.|\  .:/|
     .         +   .  .  ...:: ..|  --.:|
.      . . .   .  .  . ... :..:.."(  ..)"
 .   .       .      :  .   .: ::/  .  .::\
        """)
Example #10
0
class ExampleProtocol(DeviceProtocol):
    @request(include_msg=True)
    @return_reply(Int(min=0))
    def request_req(self, msg):
        return "ok", 3
Example #11
0
 def test_pack_types_more_types_than_args(self):
     expected = [b'one', b'2', b'1', b'four']
     self.check_packing(
         [Str(), Int(),
          Bool(default=True),
          Str(default='four')], ['one', 2], expected)
Example #12
0
 def test_request_multi(self):
     with self.assertRaises(TypeError) as ex:
         request(Bool(multiple=True), Int())
     self.assertEqual(
         str(ex.exception),
         'Only the last parameter type can accept multiple arguments.')
class BLBackendInterface(AsyncDeviceServer):
    """Breakthrough Listen's KATCP Server Backend Interface

    This server responds to requests sent from CAM, most notably:
        @ configue
        @ capture-init
        @ capture-start
        @ capture-stop
        @ capture-done
        @ deconfigure

    But because it inherits from AsyncDeviceServer, also responds to:
        * halt
        * help
        * log-level
        * restart [#restartf1]_
        * client-list
        * sensor-list
        * sensor-sampling
        * sensor-value
        * watchdog
        * version-list (only standard in KATCP v5 or later)
        * request-timeout-hint (pre-standard only if protocol flags indicates
                              timeout hints, supported for KATCP v5.1 or later)
        * sensor-sampling-clear (non-standard)
    """

    VERSION = "2020-06-19"
    DEVICE_STATUSES = ["ok", "fail", "degraded"]

    def __init__(self, server_host, server_port):
        self.port = server_port
        self.redis_server = redis.StrictRedis()
        super(BLBackendInterface, self).__init__(server_host, server_port)

    def start(self):
        """Start the server

        Based on the passed configuration object this is
        where the clients for subordinate nodes will be
        set up.
        """
        super(BLBackendInterface, self).start()
        if (sys.stdout.isatty()):
            print(R"""
                      ,'''''-._
                     ;  ,.  <> `-._
                     ;  \'   _,--'"
                    ;      (
                    ; ,   ` \
                    ;, ,     \
                   ;    |    |        MeerKAT BL Backend Interface:
                   ; |, |    |\       KATCP Server
                  ;  |  |    | \      Version: {}
                  |.-\ ,\    |\ :     Port: {}
                  |.| `. `-. | ||
                  :.|   `-. \ ';;
                   .- ,   \;;|
                   ;   ,  |  ,\       https://github.com/danielczech/meerkat-backend-interface
                   ; ,    ;    \      https://github.com/ejmichaud/meerkat-backend-interface
                  ;    , /`.  , )
               __,;,   ,'   \  ,|
         _,--''__,|   /      \  :
       ,'_,-''    | ,/        | :
      / /         | ;         ; |
     | |      __,-| |--..__,--| |---.--....___
___,-| |----''    / |         `._`-.          `----
      \ \        `'''             '''      --
       `.`.                 --'
         `.`-._        _,             ,-     __,-
            `-.`.
   --'         `;
        """.format(self.VERSION, self.port))

    @request(Str(), Str(), Int(), Str(), Str())
    @return_reply()
    def request_configure(self, req, product_id, antennas_csv, n_channels,
                          streams_json, proxy_name):
        """Receive metadata for upcoming observation.

        This command is used to configure a BLUSE instance when a 
        new subarray is activated.

        Args:
            product_id (str): This is the name of the current subarray,
                    which is used when requesting sensor data specific to 
                    components which belong to the current subarray.
                    Eg "array_1".
                    This name is unique across all current subarrays (no 
                    two concurrently active subarrays will have the same name).
                    However, it is not a globally unique identifier for all 
                    time. An identical name may be provided for later 
                    activations of other subarrays. 

            antennas_csv (str): A comma separated list of physical antenna 
                    names used in the current subarray.

            n_channels (int): The integer number of frequency channels provided
                    by the CBF.

            streams_json (str) is a JSON struct containing config keys and
                    values describing the streams.  For example:
                    {'stream_type1': {
                    'stream_name1': 'stream_address1',
                    'stream_name2': 'stream_address2',
                    ...},
                 'stream_type2': {
                    'stream_name1': 'stream_address1',
                    'stream_name2': 'stream_address2',
                    ...},
                    ...}
                The steam type keys indicate the source of the data and the 
                type, e.g. a cam.http. stream_address will be a URL.  
                For SPEAD streams, the format will be spead://<ip>[+<count>]:<port>, 
                representing SPEAD stream multicast groups.
                The count parameter indicates the number of additional consecutively 
                numbered multicast group IP addresses (sharing the same UDP port 
                number).
                stream_name is the name used to identify the stream in CAM.
                A Python example is shown below, for five streams:
                One CAM stream, with type cam.http. 
                The CAM stream provides the connection string for katportalclient 
                (for the specific subarray that this BLUSE instance is being 
                configured for).
                One F-engine stream, with type: cbf.antenna_channelised_voltage.
                One X-engine stream, with type: cbf.baseline_correlation_products.
                Two beam streams, with type: cbf.tied_array_channelised_voltage.
                The stream names ending in x are horizontally polarised, and those
                ending in y are vertically polarised.

            proxy_name (str): The CAM name for the instance of the BLUSE data
                proxy that is being configured. For example, "bluse_3".
                There will only be a single BLUSE instance of the proxy per subarray.

        Returns:
            None (responds with "ok" or "fail" and logs either info or an error).

        Writes:
            - [product_id]:timestamp" -> "1534657577373.23423" (Redis string)
            - [product_id]:antennas" -> [1,2,3,4] (Redis list)
            - [product_id]:n_channels" -> "4096" (Redis string)
            - [product_id]:proxy_name "-> "bluse_N" (Redis string)
            - [product_id]:streams" -> {....} (Redis string)
            - current:obs:id -> [product_id]
            - [product_id]:cbf_prefix -> (Redis string) 

        Publishes:
            redis-channel: 'alerts' <-- "configure"
            
        Examples:
            > ?configure array_1_bc856M4k a1,a2,a3,a4 128000 {
            "cam.http":{"camdata":"http://monctl.devnmk.camlab.kat.ac.za/api/client/2"},
            "stream_type2":{"stream_name1":"stream_address1","stream_name2":"stream_address2"}} 
            BLUSE_3
        """
        try:
            antennas_list = antennas_csv.split(",")
            json_dict = unpack_dict(streams_json)
            cam_url = json_dict['cam.http']['camdata']
        except Exception as e:
            log.error(e)
            return ("fail", e)
        # Ascertain the CBF sensor prefix (and F-engine output type).
        # Default to 'wide'; if 'wide' is not available, try 'narrow1'.
        # If neither are available, take the first available F-engine
        # output type.
        try:
            stream_type = 'cbf.antenna_channelised_voltage'
            if ('wide.antenna-channelised-voltage' in json_dict[stream_type]):
                cbf_prefix = 'wide'
                log.info('CBF prefix extracted: wide')
            elif ('narrow1.antenna-channelised-voltage'
                  in json_dict[stream_type]):
                cbf_prefix = 'narrow1'
                log.info('CBF prefix extracted: narrow1')
            else:
                cbf_prefix = next(iter(json_dict[stream_type])).split('.')[0]
                log.info('CBF prefix extracted: {}'.format(cb_prefix))
        except Exception as e:
            cbf_prefix = 'wide'
            log.error('Could not extract CBF prefix; defaulting to \'wide\'')
        statuses = []
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:timestamp".format(product_id), time.time()))
        statuses.append(
            write_list_redis(self.redis_server,
                             "{}:antennas".format(product_id), antennas_list))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:n_channels".format(product_id), n_channels))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:proxy_name".format(product_id), proxy_name))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:streams".format(product_id),
                             json.dumps(json_dict)))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:cam:url".format(product_id), cam_url))
        statuses.append(
            write_pair_redis(self.redis_server, "current:obs:id", product_id))
        statuses.append(
            write_pair_redis(self.redis_server,
                             "{}:cbf_prefix".format(product_id), cbf_prefix))
        msg = "configure:{}".format(product_id)
        statuses.append(
            publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts, msg))
        if all(statuses):
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_init(self, req, product_id):
        """Signals that an observation will start soon.
           Publishes a message to the 'alerts' channel of the form:
           capture-init:product_id
           The product_id matches that which was sent in the 
           ?configure request.
        """
        msg = "capture-init:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_start(self, req, product_id):
        """Signals that an observation is starting now.
            Publishes a message to the 'alerts' channel of the form:
            capture-start:product_id
            The product_id matches that which was sent in the 
            ?configure request.
        """
        msg = "capture-start:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_stop(self, req, product_id):
        """Signals that an observation is has stopped.
            Publishes a message to the 'alerts' channel of the form:
            capture-stop:product_id
            The product_id matches that which was sent in the 
            ?configure request.
        """
        msg = "capture-stop:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_capture_done(self, req, product_id):
        """Signals that an observation has finished.
            Publishes a message to the 'alerts' channel of the form:
            capture-done:product_id
            The product_id matches that which was sent in the 
            ?configure request.
        """
        msg = "capture-done:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    @request(Str())
    @return_reply()
    def request_deconfigure(self, req, product_id):
        """Signals that the current subarray has been deconfigured (dismantled)
           and its associated components released for use in another subarray.

           This is the signal to deconfigure the BLUSE instance associated with 
           the current subarray (and which was created by the call to ?configure 
           with the corresponding product_id). 
           Note: CAM is expected to have sent a ?capture-done request before 
           deconfiguring. If BLUSE uses an instance of katportalclient to get 
           information from CAM for the current subarray, it should disconnect.

           Publishes a message to the 'alerts' channel of the form:
           deconfigure:product_id
           The product_id matches that which was sent in the ?configure request.

           Other backend processes (such as beamformer) should be notified 
           that their data streams are ending.
        """
        msg = "deconfigure:{}".format(product_id)
        success = publish_to_redis(self.redis_server, REDIS_CHANNELS.alerts,
                                   msg)
        if success:
            return ("ok", )
        else:
            return ("fail", "Failed to publish to our local redis server")

    def setup_sensors(self):
        # TODO: Need to re-look at this function.
        """
        @brief    Set up monitoring sensors.

        @note     The following sensors are made available on top of default sensors
                  implemented in AsynDeviceServer and its base classes.

                  device-status:      Reports the health status of the proxy and associated devices:
                                      Among other things report HW failure, SW failure and observation failure.
        """
        self._device_status = Sensor.discrete(
            "device-status",
            description="Health status of BLUSE",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._device_status)

        self._local_time_synced = Sensor.boolean(
            "local-time-synced",
            description="Indicates BLUSE is NTP syncronised.",
            default=True,  # TODO: implement actual NTP synchronization request
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._local_time_synced)

        self._version = Sensor.string(
            "version",
            description="Reports the current BLUSE version",
            default=str(self.VERSION_INFO[1:]).strip('()').replace(
                ' ', '').replace(",", '.'),  # e.g. '1.0'
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._version)

    def request_halt(self, req, msg):
        """Halts the server, writes to the log, and exits the program.
        
        Returns:
            success : {'ok', 'fail'} (whether scheduling the halt succeeded).

        Examples:
            ?halt
            !halt ok

        TODO:
            - Call halt method on superclass to avoid copy paste
                Doing this caused an issue:
                    File "/Users/Eric/Berkeley/seti/packages/meerkat/lib/python2.7/site-packages/katcp/server.py", line 1102, in handle_request
                        assert (reply.mtype == Message.REPLY)
                    AttributeError: 'NoneType' object has no attribute 'mtype'
        """
        f = Future()

        @gen.coroutine
        def _halt():
            req.reply("ok")
            yield gen.moment
            self.stop(timeout=None)
            raise AsyncReply

        self.ioloop.add_callback(lambda: chain_future(_halt(), f))
        log.critical("HALTING SERVER!!!")
        # notify_slack("KATCP server at MeerKAT has halted. Might want to check that!")
        sys.exit(0)

    @request()
    @return_reply(Str())
    def request_find_alien(self, req):
        """Finds an alien.
        """
        return ("ok", R"""
.     .       .  .   . .   .   . .    +  .
  .     .  :     .    .. :. .___---------___.
       .  .   .    .  :.:. _".^ .^ ^.  '.. :"-_. .
    .  :       .  .  .:../:            . .^  :.:\.
        .   . :: +. :.:/: .   .    .        . . .:\
 .  :    .     . _ :::/:               .  ^ .  . .:\
  .. . .   . - : :.:./.                        .  .:\
  .      .     . :..|:                    .  .  ^. .:|
    .       . : : ..||        .                . . !:|
  .     . . . ::. ::\(                           . :)/
 .   .     : . : .:.|. ######              .#######::|
  :.. .  :-  : .:  ::|.#######           ..########:|
 .  .  .  ..  .  .. :\ ########          :######## :/
  .        .+ :: : -.:\ ########       . ########.:/
    .  .+   . . . . :.:\. #######       #######..:/
      :: . . . . ::.:..:.\           .   .   ..:/
   .   .   .  .. :  -::::.\.       | |     . .:/
      .  :  .  .  .-:.":.::.\             ..:/
 .      -.   . . . .: .:::.:.\.           .:/
.   .   .  :      : ....::_:..:\   ___.  :/
   .   .  .   .:. .. .  .: :.:.:\       :/
     +   .   .   : . ::. :.:. .:.|\  .:/|
     .         +   .  .  ...:: ..|  --.:|
.      . . .   .  .  . ... :..:.."(  ..)"
 .   .       .      :  .   .: ::/  .  .::\
        """)
Example #14
0
class DeviceExampleServer(katcp.DeviceServer):

    ## Interface version information.
    VERSION_INFO = ("Python CASPER packetised correlator server", 0, 1)

    ## Device server build / instance information.
    BUILD_INFO = ("corr", 0, 1, "rc2")

    #pylint: disable-msg=R0904
    def setup_sensors(self):
        pass

    def __init__(self, *args, **kwargs):
        super(DeviceExampleServer, self).__init__(*args, **kwargs)
        self.c = None  # correlator object
        self.b = None  # beamformer object

    @request(Int(default=-1))
    @return_reply(Int(), Int(), Int())
    def request_nb_set_cf(self, sock, freq):
        """Sets the center frequency for narrowband."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        try:
            rva, rvb, rvc = corr.corr_nb.channel_select(c=self.c, freq_hz=freq)
            return ("ok", rva, rvb, rvc)
        except:
            return ("fail", "Something broke spectacularly. Check the log.")

    @request(Str(default='/etc/corr/default'),
             Int(default=100),
             include_msg=True)
    @return_reply()
    def request_connect(self, sock, orgmsg, config_file, log_len):
        """Connect to all the ROACH boards. Please specify the config file and the log length. Clears any existing log. Call this again if you make external changes to the config file to reload it."""
        self.lh = corr.log_handlers.DebugLogHandler(log_len)
        try:
            self.c = corr.corr_functions.Correlator(config_file=config_file,
                                                    log_handler=self.lh,
                                                    log_level=logging.INFO)
        except Exception as err_msg:
            return ("fail", err_msg)
        try:
            self.b = corr.bf_functions.fbf(host_correlator=self.c,
                                           optimisations=True)
        except:
            self.reply_inform(
                sock,
                katcp.Message.inform(orgmsg.name, "Beamformer not available"),
                orgmsg)
            pass
        return ("ok", )

    @request(include_msg=True)
    @return_reply(Int(min=0))
    def request_get_rcs(self, sock, orgmsg):
        """Get the revision control information for the system."""
        if self.c is None:
            return katcp.Message.reply("fail",
                                       "... you haven't connected yet!")
        rcs = self.c.get_rcs()
        ret_line = []
        for e, r in rcs.iteritems():
            for k, s in r.iteritems():
                ret_line.append('%s:%s:%s' % (e, k, s))
                self.reply_inform(
                    sock, katcp.Message.inform(orgmsg.name, ret_line[-1]),
                    orgmsg)
        return ("ok", len(ret_line))

    @request(Int(default=100))
    @return_reply()
    def request_initialise(self, sock, n_retries):
        """Initialise the correlator. This programs the FPGAs, configures network interfaces etc. Includes error checks. Beamformer mode will update and add relevant functionality and SPEAD metadata. Consult the log in event of errors."""
        # First initiate the correlator
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        try:
            self.c.initialise(n_retries)
        except:
            return ("fail", "Correlator could not initialise. Check the log.")

        # Next, if beamformer object is instantiated, initiate the beamformer
        if self.b is not None:
            try:
                time.sleep(1)  # allow little time for corr init to close
                self.b.initialise()
            except:
                return ("fail",
                        "Beamformer could not initialise. Check the log.")

        return ("ok", )

    @request(include_msg=True)
    @return_reply(Int(min=0))
    def request_get_log(self, sock, orgmsg):
        """Fetch the log."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")

        print "\nlog:"
        self.lh.printMessages()

        for logent in self.c.log_handler._records:
            if logent.exc_info:
                print '%s: %s Exception: ' % (
                    logent.name, logent.msg), logent.exc_info[0:-1]
                self.reply_inform(
                    sock,
                    katcp.Message.inform(
                        "log",
                        '%s: %s Exception: ' % (logent.name, logent.msg),
                        logent.exc_info[0:-1]), orgmsg)
            else:
                #log error 1234567 basic "the error string"
                self.reply_inform(
                    sock,
                    katcp.Message.inform(
                        "log",
                        logent.levelname.lower()
                        if logent.levelname.lower() != 'warning' else 'warn',
                        '%i' % (logent.created * 1000), logent.name,
                        logent.msg), orgmsg)
                #print 'Sending this message:',logent.msg
        return ("ok", len(self.c.log_handler._records))

    @request()
    @return_reply()
    def request_clr_log(self, sock):
        """Clears the log."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        self.c.log_handler.clear()
        return ("ok", )

    @request(Int(), Str())
    @return_reply(Str())
    def request_label_input(self, sock, input_n, ant_str):
        """Label the inputs. First argument is integer specifying the physical connection. Ordering: first input of first feng, second input of first feng, ... , first input of second feng, second input of second feng, ... , second-last input of last feng, last input of last feng."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        if (input_n < self.c.config['n_inputs']):
            self.c.label_input(input_n, ant_str)
            # currently the correlator has no concept of the beamformer
            # when the correlator label_input has been updated and a beamformer is known, the SPEAD metadata for the beamformer should be re-issued as well
            # default is to issue to all beams
            if self.b is not None: self.b.spead_labelling_issue()
            return ("ok", "Input %i relabelled to %s." % (input_n, ant_str))
        else:
            #return("fail","it broke.")
            return (
                "fail",
                "Sorry, your input number is invalid. Valid range: 0 to %i." %
                (self.c.config['n_inputs'] - 1))

    @return_reply(Str(), Str())
    def request_bf_destination(self, sock, orgmsg):
        """Set destination for a given stream to a new IP address. The first argument should be the stream name, the second meta/data, the third the IP address in dotted-quad notation. An optional fourth parameters is the port."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        if self.b is None:
            return ("fail", "... no beamformer available!")
        if len(orgmsg.arguments) < 3:
            return ("fail", "... usage: <stream> <meta/data> <ip> [port]")
        stream = orgmsg.arguments[0]
        identifier = orgmsg.arguments[1]
        ip = orgmsg.arguments[2]

        if not stream in self.b.get_beams():
            return ("fail", "... name %s not a known beam!" % (stream))
        if len(ip.split('.')) != 4:
            return ("fail", "Not an expected ip address format")
        if len(orgmsg.arguments) > 3:
            try:
                port = int(orgmsg.arguments[3])
            except Exception as e:
                return ("fail", "... Exception %s" % e)
        else:
            port = None

        if string.lower(identifier) == "meta":
            self.b.config_meta_output(beams=stream,
                                      dest_ip_str=ip,
                                      dest_port=port,
                                      issue_spead=False)
        if string.lower(identifier) == "data":
            self.b.config_udp_output(beams=stream,
                                     dest_ip_str=ip,
                                     dest_port=port,
                                     issue_spead=False)
        time.sleep(3)
        return ("ok",
                "data %s:%i" % (self.b.config['bf_rx_udp_ip_str_beam%d' %
                                              self.b.beam2index(stream)[0]],
                                self.b.config['bf_rx_udp_port_beam%d' %
                                              self.b.beam2index(stream)[0]]),
                "meta %s:%i" % (self.b.config['bf_rx_meta_ip_str_beam%d' %
                                              self.b.beam2index(stream)[0]],
                                self.b.config['bf_rx_udp_port_beam%d' %
                                              self.b.beam2index(stream)[0]]))

    @return_reply(Str(), Str())
    def request_tx_start(self, sock, orgmsg):
        """Start transmission to the given IP address and port, or use the defaults from the config file if not specified. The first argument should be the IP address in dotted-quad notation. The second is the port."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        # default assumption will be correlator output
        beam = None
        dest_ip_str = None
        dest_port = None

        if len(orgmsg.arguments) > 2:
            # beamformer port
            try:
                dest_port = int(orgmsg.arguments[2])
            except Exception as e:
                return ("fail", "... %s" % e)

        if len(orgmsg.arguments) > 1:
            # second argument can be either port of ip
            if (len(orgmsg.arguments[1].split('.'))
                    == 4) and (self.b is not None):  # ip address
                dest_ip_str = orgmsg.arguments[1]
            else:
                try:
                    dest_port = int(orgmsg.arguments[1])
                except Exception as e:
                    return ("fail", "... %s" % e)

        if len(orgmsg.arguments) > 0:
            # first argument can be either stream name or ip
            if len(orgmsg.arguments[0].split('.')) == 4:  # ip address
                dest_ip_str = orgmsg.arguments[0]
            elif (self.b is None):
                return ("fail", "... no beamformer available!")
            elif (self.b is not None):  # stream name = beam name
                beam = orgmsg.arguments[0]
                if not beam in self.b.get_beams():
                    self.reply_inform(
                        sock,
                        katcp.Message.inform(
                            orgmsg.name,
                            "Name %s not a known beam, assuming correlator output"
                            % (beam)), orgmsg)
                    beam = None
        try:
            if (self.b is not None) and (beam is not None):
                self.b.config_meta_output(beams=beam,
                                          dest_ip_str=dest_ip_str,
                                          dest_port=dest_port,
                                          issue_spead=False)
                self.b.config_udp_output(beams=beam,
                                         dest_ip_str=dest_ip_str,
                                         dest_port=dest_port,
                                         issue_spead=False)
                self.b.spead_issue_all(beams=beam, from_fpga=False)
                time.sleep(
                    1)  # allow little time for meta data issue to finish
                self.b.tx_start(beams=beam)
                return ("ok", "data %s:%i" %
                        (self.b.config['bf_rx_udp_ip_str_beam%d' %
                                       self.b.beam2index(beam)[0]],
                         self.b.config['bf_rx_udp_port_beam%d' %
                                       self.b.beam2index(beam)[0]]),
                        "meta %s:%i" %
                        (self.b.config['bf_rx_meta_ip_str_beam%d' %
                                       self.b.beam2index(beam)[0]],
                         self.b.config['bf_rx_udp_port_beam%d' %
                                       self.b.beam2index(beam)[0]]))
            else:
                self.c.config_udp_output(dest_ip_str=dest_ip_str,
                                         dest_port=dest_port)
                self.c.spead_issue_all()
                self.c.tx_start()
                return ("ok", "data %s:%i" % (self.c.config['rx_udp_ip_str'],
                                              self.c.config['rx_udp_port']),
                        "meta %s:%i" % (self.c.config['rx_meta_ip_str'],
                                        self.c.config['rx_udp_port']))
        except corr.bf_functions.fbfException as be:
            return ("fail", "... %s" % be.errmsg)
        except Exception as e:
            return ("fail", "... %s" % e)


#     @request()

    @return_reply(Str())
    def request_spead_issue(self, sock, orgmsg):
        """Issue the SPEAD metadata so that the receiver can interpret the data stream.  If a beam name is specified the SPEAD metadata is issued to the requested beam, else it is issued to the correlator stream"""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")

        if len(orgmsg.arguments) > 0:
            # issue beamformer spead metadata
            beam = orgmsg.arguments[0]
            if self.b is None:
                return ("fail", "... no beams available!")
            try:
                self.b.spead_issue_all(beam)
                return ("ok", "metadata sent to %s:%i" %
                        (self.b.config['bf_rx_meta_ip_str_beam%d' %
                                       self.b.beam2index(beam)[0]],
                         self.b.config['bf_rx_udp_port_beam%d' %
                                       self.b.beam2index(beam)[0]]))
            except corr.bf_functions.fbfException as be:
                return ("fail", "... %s" % be.errmsg)
            except Exception as e:
                return (
                    "fail",
                    "Couldn't complete the request. Something broke. Check the log."
                )
        else:
            # issue correlator spead metadata
            try:
                self.c.spead_issue_all()
                return ("ok", "metadata sent to %s:%i" %
                        (self.c.config['rx_meta_ip_str'],
                         self.c.config['rx_udp_port']))
            except:
                return ("fail", "Something broke. Check the log.")

    @return_reply()
    def request_tx_stop(self, sock, orgmsg):
        """Stop transmission to the IP given in the config file."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        if len(orgmsg.arguments) > 0:
            if self.b is None:
                return (
                    'fail',
                    '... stream specification not available in correlator mode'
                )
            else:
                # beamformer tx-stop
                stream = orgmsg.arguments[0]
                try:
                    if stream in self.b.get_beams():  # stop beamformer output
                        self.reply_inform(
                            sock,
                            katcp.Message.inform(
                                orgmsg.name,
                                "Stop beamformer stream %s" % stream), orgmsg)
                        self.b.tx_stop(beams=stream)
                        return ("ok", )
                    else:  # default -- correlator status in beamformer mode
                        self.reply_inform(
                            sock,
                            katcp.Message.inform(
                                orgmsg.name,
                                "Stop correlator stream %s" % stream), orgmsg)
                        self.c.tx_stop()
                        return ("ok", )
                except corr.bf_functions.fbfException as be:
                    return ("fail", "... %s" % be.errmsg)
                except Exception as e:
                    return ("fail", "... %s" % e)
        else:
            # correlator tx-stop
            self.reply_inform(
                sock, katcp.Message.inform(orgmsg.name, "Stop correlator"),
                orgmsg)
            try:
                self.c.tx_stop()
                return ("ok", )
            except:
                return ("fail", "Something broke. Check the log.")

    @return_reply(Str())
    def request_tx_status(self, sock, orgmsg):
        """Check the TX status. Returns enabled or disabled."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        if len(orgmsg.arguments) > 0:
            if self.b is None:
                return (
                    'fail',
                    '... stream specification not available in correlator mode'
                )
            else:
                # beamformer tx-status
                stream = orgmsg.arguments[0]
                try:
                    if stream in self.b.get_beams():  # beamformer status
                        self.reply_inform(
                            sock,
                            katcp.Message.inform(
                                orgmsg.name,
                                "Beamformer status for stream %s" % stream),
                            orgmsg)
                        if self.b.tx_status_get(stream):
                            return ("ok", "enabled")
                        else:
                            return ("ok", "disabled")
                    else:  # default -- correlator status in beamformer mode
                        self.reply_inform(
                            sock,
                            katcp.Message.inform(
                                orgmsg.name,
                                "Correlator status for stream %s" % stream),
                            orgmsg)
                        if self.c.tx_status_get(): return ("ok", "enabled")
                        else: return ("ok", "disabled")
                except corr.bf_functions.fbfException as be:
                    return ("fail", "... %s" % be.errmsg)
                except Exception as e:
                    return (
                        "fail",
                        "Couldn't complete the request. Something broke. Check the log."
                    )
        else:
            # correlator tx-status
            self.reply_inform(
                sock, katcp.Message.inform(orgmsg.name, "Correlator status"),
                orgmsg)
            try:
                if self.c.tx_status_get(): return ("ok", "enabled")
                else: return ("ok", "disabled")
            except:
                return (
                    "fail",
                    "Couldn't complete the request. Something broke. Check the log."
                )

    @request(include_msg=True)
    def request_check_sys(self, sock, orgmsg):
        """Checks system health. Returns health tree informs for each engine in the system."""
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        try:
            stat = self.c.check_all(details=True)
            for l, v in stat.iteritems():
                ret_line = []
                for k, s in v.iteritems():
                    ret_line.append('%s:%s' % (k, s))
                self.reply_inform(
                    sock, katcp.Message.inform(orgmsg.name, str(l), *ret_line),
                    orgmsg)
            return katcp.Message.reply(orgmsg.name, "ok", len(stat))
        except:
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Something broke spectacularly and the check didn't complete. Scrutinise the log."
            )

    @request()
    @return_reply(Int(min=0))
    def request_resync(self, sock):
        """Rearms the system. Returns the time at which the system was synch'd in ms since unix epoch."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        try:
            time = self.c.arm()
            # currently the correlator has no concept of the beamformer
            # when the correlator label_input has been updated and a beamformer is known, the SPEAD metadata for the beamformer should be re-issued as well
            # default is to issue to all beams
            if self.b is not None: self.b.spead_time_meta_issue()
            return ("ok", (time * 1000))
        except:
            return ("fail", -1)

    def request_get_adc_snapshots(self, sock, orgmsg):
        """Grabs a snapshot of data from the specified antennas. 
            \n@Param integer Sync to 1PPS (ie align all snapshots). Note that this could cost a bit of time as we wait for the next 1PPS. 
            \n@Param integer Wait for ADC level of trigger_level to capture transients.
            \n@Params list of antenna strings.
            \n@reply str antenna name.
            \n@reply int timestamp (unix seconds since epoch) of first sample.
            \n@reply int n_samples captured since trigger."""

        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        if len(orgmsg.arguments) < 3:
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "... you didn't specify enough arguments.")
        try:
            sync_to_pps = bool(int(orgmsg.arguments[0]))
            trig_level = int(orgmsg.arguments[1])
            ant_strs = orgmsg.arguments[2:]
            for ant_str in ant_strs:
                if not ant_str in self.c.config._get_ant_mapping_list():
                    return katcp.Message.reply(
                        orgmsg.name, "fail",
                        "Antenna not found. Valid entries are %s." %
                        str(self.c.config._get_ant_mapping_list()))
            snap_data = self.c.get_adc_snapshots(ant_strs,
                                                 trig_level=trig_level,
                                                 sync_to_pps=sync_to_pps)
            for ant_str, data in snap_data.iteritems():
                self.reply_inform(
                    sock,
                    katcp.Message.inform(orgmsg.name, ant_str,
                                         str(data['timestamp'] * 1000),
                                         str(data['offset']), *data['data']),
                    orgmsg)
            return katcp.Message.reply(orgmsg.name, 'ok', str(len(snap_data)))
        except:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "something broke. sorry.")

    @request(Str(), include_msg=True)
    def request_get_adc_snapshot(self, sock, orgmsg, ant_str):
        """Grabs a snapshot of data from the antenna specified."""
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        try:
            if not ant_str in self.c.config._get_ant_mapping_list():
                return katcp.Message.reply(
                    orgmsg.name, "fail",
                    "Antenna not found. Valid entries are %s." %
                    str(self.c.config._get_ant_mapping_list()))
            unpackedBytes = self.c.get_adc_snapshots([ant_str
                                                      ])[ant_str]['data']
            return katcp.Message.reply(orgmsg.name, 'ok', *unpackedBytes)
            #return katcp.Message.reply(orgmsg.name,'ok','Awaiting rewrite!')
        except:
            return katcp.Message.reply(orgmsg.name, 'fail',
                                       "something broke. oops.")

    @request(Str(), Int(default=1), include_msg=True)
    def request_get_quant_snapshot(self, sock, orgmsg, ant_str, n_spectra):
        """Grabs a snapshot of data from the quantiser for antenna specified. Optional: number of spectra to grab (default 1)."""
        #print 'Trying to get %i spectra.'%n_spectra
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        try:
            if not (ant_str in self.c.config._get_ant_mapping_list()):
                return katcp.Message.reply(
                    orgmsg.name, "fail",
                    "Antenna not found. Valid entries are: %s" %
                    str(self.c.config._get_ant_mapping_list()))
            unpackedBytes = self.c.get_quant_snapshot(ant_str, n_spectra)
            print 'N spectra: %i.' % n_spectra
            print unpackedBytes
            if n_spectra == 1:
                self.reply_inform(
                    sock,
                    katcp.Message.inform(
                        orgmsg.name,
                        *([
                            '%i+%ij' % (val.real, val.imag)
                            for val in unpackedBytes[0]
                        ])), orgmsg)
            elif n_spectra > 1:
                for s_n, spectrum in enumerate(unpackedBytes):
                    #print 'Sending inform %i:'%s,unpackedBytes[s]
                    print 'trying to send the array:', [
                        '%i+%ij' % (val.real, val.imag)
                        for val in unpackedBytes[0][s_n]
                    ]
                    self.reply_inform(
                        sock,
                        katcp.Message.inform(
                            orgmsg.name,
                            *([
                                '%i+%ij' % (val.real, val.imag)
                                for val in unpackedBytes[0][s_n]
                            ])), orgmsg)
            else:
                raise RuntimeError(
                    "Please specify the number of spectra to be greater than zero!"
                )
            return katcp.Message.reply(orgmsg.name, 'ok', str(n_spectra))
        except:
            return katcp.Message.reply(orgmsg.name, 'fail',
                                       "something broke. darn.")

    @request(Float(min=0))
    @return_reply(Float())
    def request_acc_time(self, sock, acc_time):
        """Set the accumulation time in seconds (float)."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        try:
            running = self.c.tx_status_get()
            if running:
                self.c.tx_stop()
            act_period = self.c.acc_time_set(acc_time)
            print 'Set act time to %f' % act_period
            if running:
                self.c.tx_start()
            return ("ok", act_period)
        except:
            return (
                "fail",
                "Something broke spectacularly and the request didn't complete. Scrutinise the log."
            )

    @request(include_msg=True)
    @return_reply(Int())
    def request_get_input_levs(self, sock, orgmsg):
        """Get the current RF input levels to the DBE in dBm."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        amps = self.c.adc_amplitudes_get()
        for ant_str, ampl in amps.iteritems():
            #            rf_level=amps[i]['rms_dbm'] - self.c.rf_status_get(i)[1]
            if self.c.feng_status_get(ant_str)['adc_disabled'] == True:
                stat = 'disabled'
            elif ampl['low_level_warn'] == True:
                stat = 'low'
            elif ampl['high_level_warn'] == True:
                stat = 'high'
            else:
                stat = 'ok'
            self.reply_inform(
                sock,
                katcp.Message.inform(orgmsg.name, ant_str,
                                     "%2.2f" % ampl['input_rms_dbm'], stat),
                orgmsg)
        return ("ok", len(amps))

    @request(include_msg=True)
    @return_reply(Int())
    def request_get_ant_status(self, sock, orgmsg):
        """Decode and report the status of all connected F engines. This will automatically clear the registers after the readback."""
        if self.c is None:
            return ("fail", "... you haven't connected yet!")
        fstat = self.c.feng_status_get_all()
        self.c.rst_fstatus()
        for i in fstat:
            out_str = []
            for ent in fstat[i]:
                out_str.append(str(ent))
                out_str.append(str(fstat[i][ent]))
            self.reply_inform(sock,
                              katcp.Message.inform(orgmsg.name, i, *out_str),
                              orgmsg)
        return ("ok", len(fstat))

    @request(Str(), include_msg=True)
    def request_eq_get(self, sock, orgmsg, ant_str):
        """Get the current EQ configuration."""
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        if not ant_str in self.c.config._get_ant_mapping_list():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Antenna not found. Valid entries are %s." %
                str(self.c.config._get_ant_mapping_list()))
        eq = self.c.eq_spectrum_get(ant_str)
        return katcp.Message.reply(orgmsg.name, 'ok', *eq)

    def request_eq_set(self, sock, orgmsg):
        """Set the current EQ configuration for a given antenna. ?eq-set 0x 1123+456j 555+666j 987+765j..."""
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        ant_str = orgmsg.arguments[0]
        if not ant_str in self.c.config._get_ant_mapping_list():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Antenna not found. Valid entries are %s." %
                str(self.c.config._get_ant_mapping_list()))

        eq_coeffs = []
        if len(
                orgmsg.arguments
        ) == 2:  #+1 to account for antenna label, assume single number across entire band
            self.c.eq_spectrum_set(ant_str,
                                   init_poly=[eval(orgmsg.arguments[1])])
            return katcp.Message.reply(
                orgmsg.name, 'ok',
                "Set all coefficients to %i." % eval(orgmsg.arguments[1]))
        elif len(orgmsg.arguments) != (self.c.config['n_chans'] +
                                       1):  #+1 to account for antenna label
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Sorry, you didn't specify the right number of coefficients (expecting %i, got %i)."
                % (self.c.config['n_chans'], len(orgmsg.arguments) - 1))
        else:
            for arg in orgmsg.arguments[1:]:
                eq_coeffs.append(eval(arg))
            self.c.eq_spectrum_set(ant_str, init_coeffs=eq_coeffs)
            return katcp.Message.reply(orgmsg.name, 'ok')

    def request_fr_delay_set(self, sock, orgmsg):
        """Set the fringe rate and delay compensation config for a given antenna. Parms: antpol, fringe_offset (degrees), fr_rate (Hz), delay (seconds), delay rate, load_time (Unix seconds), <ignore check>. If there is a seventh argument, don't do any checks to see if things loaded properly. If the load time is negative, load asap."""
        if self.c is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        ant_str = orgmsg.arguments[0]
        if not ant_str in self.c.config._get_ant_mapping_list():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Antenna not found. Valid entries are %s." %
                str(self.c.config._get_ant_mapping_list()))

        fr_offset = float(orgmsg.arguments[1])
        fr_rate = float(orgmsg.arguments[2])
        delay = float(orgmsg.arguments[3])
        del_rate = float(orgmsg.arguments[4])
        ld_time = float(orgmsg.arguments[5])

        if len(orgmsg.arguments) > 6:
            ld_check = False
        #    print 'Ignoring load check.'
        else:
            ld_check = True
        #    print 'Check for correct load.'

        stat = self.c.fr_delay_set(ant_str,
                                   fringe_phase=fr_offset,
                                   fringe_rate=fr_rate,
                                   delay=delay,
                                   delay_rate=del_rate,
                                   ld_time=ld_time,
                                   ld_check=ld_check)
        out_str = []
        for ent in stat:
            out_str.append(str(ent))
            out_str.append("%12.10e" % (stat[ent]))

        return katcp.Message.reply(orgmsg.name, 'ok', *out_str)

    @request(Str(), Float(default=-1), Float(default=-1), include_msg=True)
    @return_reply(Float(), Float())
    def request_beam_passband(self, sock, orgmsg, beam, bw, cf):
        """Setup of beamformer output passband. Please specify a beam name to return the current bandwidth and centre frequency in Hz. Alternatively, specify a beam name, bandwidth and centre frequency in Hz to set the beamformer output passband. The closest actual bandwidth and centre frequency achievable will be returned."""
        if self.b is None:
            return ("fail", "... beamformer functionality only!")

        if bw >= 0 and cf >= 0:
            try:
                self.b.set_passband(beams=beam,
                                    bandwidth=bw,
                                    centre_frequency=cf)
            except corr.bf_functions.fbfException as be:
                return ("fail", "... %s" % be.errmsg)
            except Exception as e:
                return ("fail", "... %s" % e)
        elif (bw * cf < 0):
            return (
                "fail",
                "... require both bandwidth and center frequency to be specified"
            )

        try:
            cf, bw = self.b.get_passband(beam=beam)
            return ("ok", bw, cf)
        except:
            return ("fail", "... unknown beam name %s" % orgmsg.arguments[0])

    @request(Str(), Str(), include_msg=True)
    def request_weights_get(self, sock, orgmsg, beam, ant_str):
        """Get the current beamformer weights. Params: beam, ant_str"""
        if self.c is None or self.b is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        if not ant_str in self.c.config._get_ant_mapping_list():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Antenna not found. Valid entries are %s." %
                str(self.c.config._get_ant_mapping_list()))
        if not beam in self.b.get_beams():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Unknown beam name. Valid entries are %s." %
                str(self.b.get_beams()))
        try:
            weights = self.b.cal_spectrum_get(beam=beam, ant_str=ant_str)
            return katcp.Message.reply(orgmsg.name, 'ok', *weights)
        except corr.bf_functions.fbfException as be:
            return ("fail", "... %s" % be.errmsg)
        except Exception as e:
            return ("fail", "... %s" % e)

    def request_weights_set(self, sock, orgmsg):
        """Set the weights for an input to a selected beam. ?weights-set bf0 0x 1123+456j 555+666j 987+765j..."""
        if self.c is None or self.b is None:
            return katcp.Message.reply(orgmsg.name, "fail",
                                       "... you haven't connected yet!")
        beam = orgmsg.arguments[0]
        if not beam in self.b.get_beams():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Unknown beam name. Valid entries are %s." %
                str(self.b.get_beams()))
        ant_str = orgmsg.arguments[1]
        if not ant_str in self.c.config._get_ant_mapping_list():
            return katcp.Message.reply(
                orgmsg.name, "fail",
                "Antenna not found. Valid entries are %s." %
                str(self.c.config._get_ant_mapping_list()))
        try:
            bw_coeffs = []
            if len(
                    orgmsg.arguments
            ) == 3:  #+2 to account for beam name and antenna label, assume single number across entire band
                self.b.cal_spectrum_set(beam=beam,
                                        ant_str=ant_str,
                                        init_poly=[eval(orgmsg.arguments[2])])
                return katcp.Message.reply(orgmsg.name, 'ok',
                                           "Set all coefficients to",
                                           eval(orgmsg.arguments[2]))
            elif len(orgmsg.arguments) != (
                    self.c.config['n_chans'] +
                    2):  #+2 to account for beam name and antenna label
                return katcp.Message.reply(
                    orgmsg.name, "fail",
                    "Sorry, you didn't specify the right number of coefficients (expecting %i, got %i)."
                    % (self.c.config['n_chans'], len(orgmsg.arguments) - 2))
            else:
                for arg in orgmsg.arguments[2:]:
                    bw_coeffs.append(eval(arg))
                self.b.cal_spectrum_set(beam=beam,
                                        ant_str=ant_str,
                                        init_coeffs=bw_coeffs)
                return katcp.Message.reply(orgmsg.name, 'ok')
        except corr.bf_functions.fbfException as be:
            return ("fail", "... %s" % be.errmsg)
        except Exception as e:
            return ("fail", "... %s" % e)
Example #15
0
class MasterController(AsyncDeviceServer):
    """This is the main KATCP interface for the FBFUSE
    multi-beam beamformer on MeerKAT.

    This interface satisfies the following ICDs:
    CAM-FBFUSE: <link>
    TUSE-FBFUSE: <link>
    """
    VERSION_INFO = ("mpikat-api", 0, 1)
    BUILD_INFO = ("mpikat-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]

    def __init__(self, ip, port, worker_pool):
        """
        @brief       Construct new MasterController instance

        @params  ip       The IP address on which the server should listen
        @params  port     The port that the server should bind to
        """
        super(MasterController, self).__init__(ip, port)
        self._products = {}
        self._server_pool = worker_pool

    def start(self):
        """
        @brief  Start the MasterController server
        """
        super(MasterController, self).start()

    def stop(self):
        self._ntp_callback.stop()
        super(MasterController, self).stop()

    def add_sensor(self, sensor):
        log.debug("Adding sensor: {}".format(sensor.name))
        super(MasterController, self).add_sensor(sensor)

    def remove_sensor(self, sensor):
        log.debug("Removing sensor: {}".format(sensor.name))
        super(MasterController, self).remove_sensor(sensor)

    def setup_sensors(self):
        """
        @brief  Set up monitoring sensors.

        @note   The following sensors are made available on top of default sensors
                implemented in AsynDeviceServer and its base classes.

                device-status:  Reports the health status of the controller and associated devices:
                                Among other things report HW failure, SW failure and observation failure.

                local-time-synced:  Indicates whether the local time of the servers
                                    is synchronised to the master time reference (use NTP).
                                    This sensor is aggregated from all nodes that are part
                                    of FBF and will return "not sync'd" if any nodes are
                                    unsyncronised.

                products:   The list of product_ids that controller is currently handling
        """
        self._device_status = Sensor.discrete(
            "device-status",
            description="Health status of FBFUSE",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._device_status)

        self._local_time_synced = Sensor.boolean(
            "local-time-synced",
            description="Indicates FBF is NTP syncronised.",
            default=True,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._local_time_synced)

        def ntp_callback():
            log.debug("Checking NTP sync")
            try:
                synced = check_ntp_sync()
            except Exception:
                log.exception("Unable to check NTP sync")
                self._local_time_synced.set_value(False)
            else:
                if not synced:
                    log.warning("Server is not NTP synced")
                self._local_time_synced.set_value(synced)

        ntp_callback()
        self._ntp_callback = PeriodicCallback(ntp_callback,
                                              NTP_CALLBACK_PERIOD)
        self._ntp_callback.start()

        self._products_sensor = Sensor.string(
            "products",
            description="The names of the currently configured products",
            default="",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._products_sensor)

    def _update_products_sensor(self):
        self._products_sensor.set_value(",".join(self._products.keys()))

    def _get_product(self, product_id):
        if product_id not in self._products:
            raise ProductLookupError(
                "No product configured with ID: {}".format(product_id))
        else:
            return self._products[product_id]

    @request(Str(), Int())
    @return_reply()
    def request_register_worker_server(self, req, hostname, port):
        """
        @brief   Register an WorkerWrapper instance

        @params hostname The hostname for the worker server
        @params port     The port number that the worker server serves on

        @detail  Register an WorkerWrapper instance that can be used for FBFUSE
                 computation. FBFUSE has no preference for the order in which control
                 servers are allocated to a subarray. An WorkerWrapper wraps an atomic
                 unit of compute comprised of one CPU, one GPU and one NIC (i.e. one NUMA
                 node on an FBFUSE compute server).
        """
        log.debug("Received request to register worker server at {}:{}".format(
            hostname, port))
        self._server_pool.add(hostname, port)
        return ("ok", )

    @request(Str(), Int())
    @return_reply()
    def request_deregister_worker_server(self, req, hostname, port):
        """
        @brief   Deregister an WorkerWrapper instance

        @params hostname The hostname for the worker server
        @params port     The port number that the worker server serves on

        @detail  The graceful way of removing a server from rotation. If the server is
                 currently actively processing an exception will be raised.
        """
        log.debug(
            "Received request to deregister worker server at {}:{}".format(
                hostname, port))
        try:
            self._server_pool.remove(hostname, port)
        except ServerDeallocationError as error:
            log.error(
                "Request to deregister worker server at {}:{} failed with error: {}"
                .format(hostname, port, str(error)))
            return ("fail", str(error))
        else:
            return ("ok", )

    @request()
    @return_reply(Int())
    def request_worker_server_list(self, req):
        """
        @brief   List all control servers and provide minimal metadata
        """
        for server in self._server_pool.used():
            req.inform("{} allocated".format(server))
        for server in self._server_pool.available():
            req.inform("{} free".format(server))
        return ("ok", len(self._server_pool.used()) +
                len(self._server_pool.available()))

    @request()
    @return_reply(Int())
    def request_product_list(self, req):
        """
        @brief      List all currently registered products and their states

        @param      req               A katcp request object

        @note       The details of each product are provided via an #inform
                    as a JSON string containing information on the product state.

        @return     katcp reply object [[[ !product-list ok | (fail [error description]) <number of configured products> ]]],
        """
        for product_id, product in self._products.items():
            info = {}
            info[product_id] = product.info()
            as_json = json.dumps(info)
            req.inform(as_json)
        return ("ok", len(self._products))
Example #16
0
 def test_return_reply_multi(self):
     with self.assertRaises(TypeError) as ex:
         return_reply(Bool(multiple=True), Int())
     self.assertEqual(
         ex.exception.message,
         'Only the last parameter type can accept multiple arguments.')
Example #17
0
class FbfMasterController(MasterController):
    """This is the main KATCP interface for the FBFUSE
    multi-beam beamformer on MeerKAT.

    This interface satisfies the following ICDs:
    CAM-FBFUSE: <link>
    TUSE-FBFUSE: <link>
    """
    VERSION_INFO = ("mpikat-fbf-api", 0, 1)
    BUILD_INFO = ("mpikat-fbf-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]
    def __init__(self, ip, port, dummy=True,
        ip_range = FBF_IP_RANGE):
        """
        @brief       Construct new FbfMasterController instance

        @params  ip       The IP address on which the server should listen
        @params  port     The port that the server should bind to
        @params  dummy    Specifies if the instance is running in a dummy mode

        @note   In dummy mode, the controller will act as a mock interface only, sending no requests to nodes.
                A valid node pool must still be provided to the instance, but this may point to non-existent nodes.

        """
        self._ip_pool = IpRangeManager(ip_range_from_stream(ip_range))
        super(FbfMasterController, self).__init__(ip, port, FbfWorkerPool())
        self._dummy = dummy
        if self._dummy:
            for ii in range(64):
                self._server_pool.add("127.0.0.1", 50000+ii)


    def setup_sensors(self):
        """
        @brief  Set up monitoring sensors.
        """
        super(FbfMasterController, self).setup_sensors()
        self._ip_pool_sensor = Sensor.string(
            "output-ip-range",
            description="The multicast address allocation for coherent beams",
            default=self._ip_pool.format_katcp(),
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._ip_pool_sensor)

    @request(Str(), Str(), Int(), Str(), Str())
    @return_reply()
    def request_configure(self, req, product_id, antennas_csv, n_channels, streams_json, proxy_name):
        """
        @brief      Configure FBFUSE to receive and process data from a subarray

        @detail     REQUEST ?configure product_id antennas_csv n_channels streams_json proxy_name
                    Configure FBFUSE for the particular data products

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, which is a useful tag to include
                                      in the data, but should not be analysed further. For example "array_1_bc856M4k".

        @param      antennas_csv      A comma separated list of physical antenna names used in particular sub-array
                                      to which the data products belongs (e.g. m007,m008,m009).

        @param      n_channels        The integer number of frequency channels provided by the CBF.

        @param      streams_json      a JSON struct containing config keys and values describing the streams.

                                      For example:

                                      @code
                                         {'stream_type1': {
                                             'stream_name1': 'stream_address1',
                                             'stream_name2': 'stream_address2',
                                             ...},
                                             'stream_type2': {
                                             'stream_name1': 'stream_address1',
                                             'stream_name2': 'stream_address2',
                                             ...},
                                          ...}
                                      @endcode

                                      The steam type keys indicate the source of the data and the type, e.g. cam.http.
                                      stream_address will be a URI.  For SPEAD streams, the format will be spead://<ip>[+<count>]:<port>,
                                      representing SPEAD stream multicast groups. When a single logical stream requires too much bandwidth
                                      to accommodate as a single multicast group, the count parameter indicates the number of additional
                                      consecutively numbered multicast group ip addresses, and sharing the same UDP port number.
                                      stream_name is the name used to identify the stream in CAM.
                                      A Python example is shown below, for five streams:
                                      One CAM stream, with type cam.http.  The camdata stream provides the connection string for katportalclient
                                      (for the subarray that this FBFUSE instance is being configured on).
                                      One F-engine stream, with type:  cbf.antenna_channelised_voltage.
                                      One X-engine stream, with type:  cbf.baseline_correlation_products.
                                      Two beam streams, with type: cbf.tied_array_channelised_voltage.  The stream names ending in x are
                                      horizontally polarised, and those ending in y are vertically polarised.

                                      @code
                                         pprint(streams_dict)
                                         {'cam.http':
                                             {'camdata':'http://10.8.67.235/api/client/1'},
                                          'cbf.antenna_channelised_voltage':
                                             {'i0.antenna-channelised-voltage':'spead://239.2.1.150+15:7148'},
                                          ...}
                                      @endcode

                                      If using katportalclient to get information from CAM, then reconnect and re-subscribe to all sensors
                                      of interest at this time.

        @param      proxy_name        The CAM name for the instance of the FBFUSE data proxy that is being configured.
                                      For example, "FBFUSE_3".  This can be used to query sensors on the correct proxy,
                                      in the event that there are multiple instances in the same subarray.

        @note       A configure call will result in the generation of a new subarray instance in FBFUSE that will be added to the clients list.

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """

        msg = ("Configuring new FBFUSE product",
            "Product ID: {}".format(product_id),
            "Antennas: {}".format(antennas_csv),
            "Nchannels: {}".format(n_channels),
            "Streams: {}".format(streams_json),
            "Proxy name: {}".format(proxy_name))
        log.info("\n".join(msg))
        # Test if product_id already exists
        if product_id in self._products:
            return ("fail", "FBF already has a configured product with ID: {}".format(product_id))
        # Determine number of nodes required based on number of antennas in subarray
        # Note this is a poor way of handling this that may be updated later. In theory
        # there is a throughput measure as a function of bandwidth, polarisations and number
        # of antennas that allows one to determine the number of nodes to run. Currently we
        # just assume one antennas worth of data per NIC on our servers, so two antennas per
        # node.
        try:
            antennas = parse_csv_antennas(antennas_csv)
        except AntennaValidationError as error:
            return ("fail", str(error))

        valid_n_channels = [1024, 4096, 32768]
        if not n_channels in valid_n_channels:
            return ("fail", "The provided number of channels ({}) is not valid. Valid options are {}".format(n_channels, valid_n_channels))

        streams = json.loads(streams_json)
        try:
            streams['cam.http']['camdata']
            # Need to check for endswith('.antenna-channelised-voltage') as the i0 is not
            # guaranteed to stay the same.
            # i0 = instrument name
            # Need to keep this for future sensor lookups
            streams['cbf.antenna_channelised_voltage']
        except KeyError as error:
            return ("fail", "JSON streams object does not contain required key: {}".format(str(error)))

        for key, value in streams['cbf.antenna_channelised_voltage'].items():
            if key.endswith('.antenna-channelised-voltage'):
                instrument_name, _ = key.split('.')
                feng_stream_name = key
                feng_groups = value
                log.debug("Parsed instrument name from streams: {}".format(instrument_name))
                break
        else:
            return ("fail", "Could not determine instrument name (e.g. 'i0') from streams")

        # TODO: change this request to @async_reply and make the whole thing a coroutine
        @coroutine
        def configure():
            kpc = self._katportal_wrapper_type(streams['cam.http']['camdata'])
            # Get all antenna observer strings
            futures, observers = [],[]
            for antenna in antennas:
                log.debug("Fetching katpoint string for antenna {}".format(antenna))
                futures.append(kpc.get_observer_string(antenna))
            for ii,future in enumerate(futures):
                try:
                    observer = yield future
                except Exception as error:
                    log.error("Error on katportalclient call: {}".format(str(error)))
                    req.reply("fail", "Error retrieving katpoint string for antenna {}".format(antennas[ii]))
                    return
                else:
                    log.debug("Fetched katpoint antenna: {}".format(observer))
                    observers.append(Antenna(observer))

            # Get bandwidth, cfreq, sideband, f-eng mapping

            #TODO: Also get sync-epoch

            log.debug("Fetching F-engine and subarray configuration information")
            bandwidth_future = kpc.get_bandwidth(feng_stream_name)
            cfreq_future = kpc.get_cfreq(feng_stream_name)
            sideband_future = kpc.get_sideband(feng_stream_name)
            feng_antenna_map_future = kpc.get_antenna_feng_id_map(instrument_name, antennas)
            sync_epoch_future = kpc.get_sync_epoch()
            bandwidth = yield bandwidth_future
            cfreq = yield cfreq_future
            sideband = yield sideband_future
            feng_antenna_map = yield feng_antenna_map_future
            sync_epoch = yield sync_epoch_future
            feng_config = {
                'bandwidth': bandwidth,
                'centre-frequency': cfreq,
                'sideband': sideband,
                'feng-antenna-map': feng_antenna_map,
                'sync-epoch': sync_epoch,
                'nchans': n_channels
            }
            for key, value in feng_config.items():
                log.debug("{}: {}".format(key, value))
            product = FbfProductController(self, product_id, observers, n_channels,
                feng_groups, proxy_name, feng_config)
            self._products[product_id] = product
            self._update_products_sensor()
            req.reply("ok",)
            log.debug("Configured FBFUSE instance with ID: {}".format(product_id))
        self.ioloop.add_callback(configure)
        raise AsyncReply

    @request(Str())
    @return_reply()
    def request_deconfigure(self, req, product_id):
        """
        @brief      Deconfigure the FBFUSE instance.

        @note       Deconfigure the FBFUSE instance. If FBFUSE uses katportalclient to get information
                    from CAM, then it should disconnect at this time.

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, used to track which subarray is being deconfigured.
                                      For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !deconfigure ok | (fail [error description]) ]]]
        """
        log.info("Deconfiguring FBFUSE instace with ID '{}'".format(product_id))
        # Test if product exists
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        try:
            product.deconfigure()
        except Exception as error:
            return ("fail", str(error))
        del self._products[product_id]
        self._update_products_sensor()
        return ("ok",)


    @request(Str(), Str())
    @return_reply()
    @coroutine
    def request_target_start(self, req, product_id, target):
        """
        @brief      Notify FBFUSE that a new target is being observed

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      target          A KATPOINT target string

        @return     katcp reply object [[[ !target-start ok | (fail [error description]) ]]]
        """
        log.info("Received new target: {}".format(target))
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            raise Return(("fail", str(error)))
        try:
            target = Target(target)
        except Exception as error:
            raise Return(("fail", str(error)))
        yield product.target_start(target)
        raise Return(("ok",))


    # DELETE this

    @request(Str())
    @return_reply()
    @coroutine
    def request_target_stop(self, req, product_id):
        """
        @brief      Notify FBFUSE that the telescope has stopped observing a target

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !target-start ok | (fail [error description]) ]]]
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            raise Return(("fail", str(error)))
        yield product.target_stop()
        raise Return(("ok",))


    @request(Str())
    @return_reply()
    def request_capture_init(self, req, product_id):
        """NOOP"""
        return ("ok",)

    @request(Str())
    @return_reply()
    def request_capture_done(self, req, product_id):
        """NOOP"""
        return ("ok",)

    @request(Str())
    @return_reply()
    def request_capture_start(self, req, product_id):
        """
        @brief      Request that FBFUSE start beams streaming

        @detail     Upon this call the provided coherent and incoherent beam configurations will be evaluated
                    to determine if they are physical and can be met with the existing hardware. If the configurations
                    are acceptable then servers allocated to this instance will be triggered to begin production of beams.

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, used to track which subarray is being deconfigured.
                                      For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !start-beams ok | (fail [error description]) ]]]
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        @coroutine
        def start():
            try:
                product.capture_start()
            except Exception as error:
                req.reply("fail", str(error))
            else:
                req.reply("ok",)
        self.ioloop.add_callback(start)
        raise AsyncReply

    @request(Str(), Str())
    @return_reply()
    def request_provision_beams(self, req, product_id, sb_id):
        """
        @brief      Request that FBFUSE asynchronously prepare to start beams streaming

        @detail     Upon this call the provided coherent and incoherent beam configurations will be evaluated
                    to determine if they are physical and can be met with the existing hardware. If the configurations
                    are acceptable then servers allocated to this instance will be triggered to prepare for the production of beams.
                    Unlike a call to ?capture-start, ?provision-beams will not trigger a connection to multicast groups and will not
                    wait for completion before returning, instead it will start the process of beamformer resource alloction and compilation.
                    To determine when the process is complete, the user must wait on the value of the product "state" sensor becoming "ready",
                    e.g.

                    @code
                        client.sensor['{}-state'.format(proxy_name)].wait(
                            lambda reading: reading.value == 'ready')
                    @endcode

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, used to track which subarray is being deconfigured.
                                      For example "array_1_bc856M4k".

        @param      sb_id             Schedule block ID for the commencing schedule block

        @return     katcp reply object [[[ !start-beams ok | (fail [error description]) ]]]
        """
        # Note: the state of the product won't be updated until the start call hits the top of the
        # event loop. It may be preferable to keep a self.starting_future object and yield on it
        # in capture-start if it exists. The current implementation may or may not be a bug...
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        # This check needs to happen here as this call
        # should return immediately
        if not product.idle:
            return ("fail", "Can only provision beams on an idle FBF product")
        self.ioloop.add_callback(lambda : product.prepare(sb_id))
        return ("ok",)

    @request(Str())
    @return_reply()
    def request_capture_stop(self, req, product_id):
        """
        @brief      Stop FBFUSE streaming

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        @coroutine
        def stop():
            product.capture_stop()
            req.reply("ok",)
        self.ioloop.add_callback(stop)
        raise AsyncReply

    @request(Str(), Str(), Int())
    @return_reply()
    def request_set_configuration_authority(self, req, product_id, hostname, port):
        """
        @brief     Set the configuration authority for an FBF product

        @detail    The parameters passed here specify the address of a server that
                   can be triggered to provide FBFUSE with configuration information
                   at schedule block and target boundaries. The configuration authority
                   must be a valid KATCP server.
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        product.set_configuration_authority(hostname, port)
        return ("ok",)

    @request(Str())
    @return_reply()
    def request_reset_beams(self, req, product_id):
        """
        @brief      Reset the positions of all allocated beams

        @note       This call may only be made AFTER a successful call to start-beams. Before this point no beams are
                    allocated to the instance. If all beams are currently allocated an exception will be raised.

        @param      req             A katcp request object

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !reset-beams m ok | (fail [error description]) ]]]
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        else:
            beam = product.reset_beams()
            return ("ok", )

    @request(Str(), Str())
    @return_reply(Str())
    def request_add_beam(self, req, product_id, target):
        """
        @brief      Configure the parameters of one beam

        @note       This call may only be made AFTER a successful call to start-beams. Before this point no beams are
                    allocated to the instance. If all beams are currently allocated an exception will be raised.

        @param      req             A katcp request object

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      target          A KATPOINT target string

        @return     katcp reply object [[[ !add-beam ok | (fail [error description]) ]]]
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        try:
            target = Target(target)
        except Exception as error:
            return ("fail", str(error))
        beam = product.add_beam(target)
        return ("ok", beam.idx)

    @request(Str(), Str(), Int(), Float(), Float(), Float())
    @return_reply(Str())
    def request_add_tiling(self, req, product_id, target, nbeams, reference_frequency, overlap, epoch):
        """
        @brief      Configure the parameters of a static beam tiling

        @note       This call may only be made AFTER a successful call to start-beams. Before this point no beams are
                    allocated to the instance. If there are not enough free beams to satisfy the request an
                    exception will be raised.

        @note       Beam shapes calculated for tiling are always assumed to be 2D elliptical Gaussians.

        @param      req             A katcp request object

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      target          A KATPOINT target string

        @param      nbeams          The number of beams in this tiling pattern.

        @param      reference_frequency     The reference frequency at which to calculate the synthesised beam shape,
                                            and thus the tiling pattern. Typically this would be chosen to be the
                                            centre frequency of the current observation.

        @param      overlap         The desired overlap point between beams in the pattern. The overlap defines
                                    at what power point neighbouring beams in the tiling pattern will meet. For
                                    example an overlap point of 0.1 corresponds to beams overlapping only at their
                                    10%-power points. Similarly a overlap of 0.5 corresponds to beams overlapping
                                    at their half-power points. [Note: This is currently a tricky parameter to use
                                    when values are close to zero. In future this may be define in sigma units or
                                    in multiples of the FWHM of the beam.]

        @param      epoch           The desired epoch for the tiling pattern as a unix time. A typical usage would
                                    be to set the epoch to half way into the coming observation in order to minimise
                                    the effect of parallactic angle and array projection changes altering the shape
                                    and position of the beams and thus changing the efficiency of the tiling pattern.


        @return     katcp reply object [[[ !add-tiling ok | (fail [error description]) ]]]
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        try:
            target = Target(target)
        except Exception as error:
            return ("fail", str(error))
        tiling = product.add_tiling(target, nbeams, reference_frequency, overlap, epoch)
        return ("ok", tiling.idxs())

    @request()
    @return_reply(Int())
    def request_product_list(self, req):
        """
        @brief      List all currently registered products and their states

        @param      req               A katcp request object

        @note       The details of each product are provided via an #inform
                    as a JSON string containing information on the product state.

        @return     katcp reply object [[[ !product-list ok | (fail [error description]) <number of configured products> ]]],
        """
        for product_id,product in self._products.items():
            info = {}
            info[product_id] = product.info()
            as_json = json.dumps(info)
            req.inform(as_json)
        return ("ok",len(self._products))

    @request(Str(), Str())
    @return_reply()
    def request_set_default_target_configuration(self, req, product_id, target):
        """
        @brief      Set the configuration of FBFUSE from the FBFUSE configuration server

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      target          A KATPOINT target string
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        try:
            target = Target(target)
        except Exception as error:
            return ("fail", str(error))
        if not product.capturing:
            return ("fail","Product must be capturing before a target confiugration can be set.")
        product.reset_beams()
        # TBD: Here we connect to some database and request the default configurations
        # For example this may return secondary target in the FoV
        #
        # As a default the current system will put one beam directly on target and
        # the rest of the beams in a static tiling pattern around this target
        now = time.time()
        nbeams = product._beam_manager.nbeams
        product.add_tiling(target, nbeams-1, 1.4e9, 0.5, now)
        product.add_beam(target)
        return ("ok",)

    @request(Str(), Str())
    @return_reply()
    def request_set_default_sb_configuration(self, req, product_id, sb_id):
        """
        @brief      Set the configuration of FBFUSE from the FBFUSE configuration server

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      sb_id           The schedule block ID. Decisions of the configuarion of FBFUSE will be made dependent on
                                    the configuration of the current subarray, the primary and secondary science projects
                                    active and the targets expected to be visted during the execution of the schedule block.
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        if product.capturing:
            return ("fail", "Cannot reconfigure a currently capturing instance.")
        product.configure_coherent_beams(400, product._katpoint_antennas, 1, 16)
        product.configure_incoherent_beam(product._katpoint_antennas, 1, 16)
        now = time.time()
        nbeams = product._beam_manager.nbeams
        product.add_tiling(target, nbeams-1, 1.4e9, 0.5, now)
        product.add_beam(target)
        return ("ok",)
Example #18
0
 def test_unpack_types_many_without_multiple(self):
     expected = ['one', 2]
     self.check_unpacking([Str(), Int()], [b'one', b'2'], expected)
Example #19
0
class MasterController(AsyncDeviceServer):
    """This is the main KATCP interface for the FBFUSE
    multi-beam beamformer on MeerKAT.

    This interface satisfies the following ICDs:
    CAM-FBFUSE: <link>
    TUSE-FBFUSE: <link>
    """
    VERSION_INFO = ("mpikat-api", 0, 1)
    BUILD_INFO = ("mpikat-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]

    def __init__(self, ip, port, worker_pool):
        """
        @brief       Construct new MasterController instance

        @params  ip       The IP address on which the server should listen
        @params  port     The port that the server should bind to
        """
        super(MasterController, self).__init__(ip, port)
        self._products = {}
        self._katportal_wrapper_type = KatportalClientWrapper
        self._server_pool = worker_pool

    def start(self):
        """
        @brief  Start the MasterController server
        """
        super(MasterController, self).start()

    def add_sensor(self, sensor):
        log.debug("Adding sensor: {}".format(sensor.name))
        super(MasterController, self).add_sensor(sensor)

    def remove_sensor(self, sensor):
        log.debug("Removing sensor: {}".format(sensor.name))
        super(MasterController, self).remove_sensor(sensor)

    def setup_sensors(self):
        """
        @brief  Set up monitoring sensors.

        @note   The following sensors are made available on top of default sensors
                implemented in AsynDeviceServer and its base classes.

                device-status:  Reports the health status of the controller and associated devices:
                                Among other things report HW failure, SW failure and observation failure.

                local-time-synced:  Indicates whether the local time of the servers
                                    is synchronised to the master time reference (use NTP).
                                    This sensor is aggregated from all nodes that are part
                                    of FBF and will return "not sync'd" if any nodes are
                                    unsyncronised.

                products:   The list of product_ids that controller is currently handling
        """
        self._device_status = Sensor.discrete(
            "device-status",
            description="Health status of FBFUSE",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._device_status)

        self._local_time_synced = Sensor.boolean(
            "local-time-synced",
            description="Indicates FBF is NTP syncronised.",
            default=True,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._local_time_synced)

        def ntp_callback():
            try:
                synced = check_ntp_sync()
            except Exception as error:
                log.exception("Unable to check NTP sync")
                self._local_time_synced.set_value(False)
            else:
                if not synced:
                    log.warning("Server is not NTP synced")
                self._local_time_synced.set_value(synced)

        self._ntp_callback = PeriodicCallback(ntp_callback,
                                              NTP_CALLBACK_PERIOD)
        self._ntp_callback.start()

        self._products_sensor = Sensor.string(
            "products",
            description="The names of the currently configured products",
            default="",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._products_sensor)

    def _update_products_sensor(self):
        self._products_sensor.set_value(",".join(self._products.keys()))

    def _get_product(self, product_id):
        if product_id not in self._products:
            raise ProductLookupError(
                "No product configured with ID: {}".format(product_id))
        else:
            return self._products[product_id]

    @request(Str(), Int())
    @return_reply()
    def request_register_worker_server(self, req, hostname, port):
        """
        @brief   Register an WorkerWrapper instance

        @params hostname The hostname for the worker server
        @params port     The port number that the worker server serves on

        @detail  Register an WorkerWrapper instance that can be used for FBFUSE
                 computation. FBFUSE has no preference for the order in which control
                 servers are allocated to a subarray. An WorkerWrapper wraps an atomic
                 unit of compute comprised of one CPU, one GPU and one NIC (i.e. one NUMA
                 node on an FBFUSE compute server).
        """
        log.debug("Received request to register worker server at {}:{}".format(
            hostname, port))
        self._server_pool.add(hostname, port)
        return ("ok", )

    @request(Str(), Int())
    @return_reply()
    def request_deregister_worker_server(self, req, hostname, port):
        """
        @brief   Deregister an WorkerWrapper instance

        @params hostname The hostname for the worker server
        @params port     The port number that the worker server serves on

        @detail  The graceful way of removing a server from rotation. If the server is
                 currently actively processing an exception will be raised.
        """
        log.debug(
            "Received request to deregister worker server at {}:{}".format(
                hostname, port))
        try:
            self._server_pool.remove(hostname, port)
        except ServerDeallocationError as error:
            log.error(
                "Request to deregister worker server at {}:{} failed with error: {}"
                .format(hostname, port, str(error)))
            return ("fail", str(error))
        else:
            return ("ok", )

    @request()
    @return_reply(Int())
    def request_worker_server_list(self, req):
        """
        @brief   List all control servers and provide minimal metadata
        """
        for server in self._server_pool.used():
            req.inform("{} allocated".format(server))
        for server in self._server_pool.available():
            req.inform("{} free".format(server))
        return ("ok", len(self._server_pool.used()) +
                len(self._server_pool.available()))

    @request()
    @return_reply(Int())
    def request_product_list(self, req):
        """
        @brief      List all currently registered products and their states

        @param      req               A katcp request object

        @note       The details of each product are provided via an #inform
                    as a JSON string containing information on the product state.

        @return     katcp reply object [[[ !product-list ok | (fail [error description]) <number of configured products> ]]],
        """
        for product_id, product in self._products.items():
            info = {}
            info[product_id] = product.info()
            as_json = json.dumps(info)
            req.inform(as_json)
        return ("ok", len(self._products))

    @request(Str(), Str())
    @return_reply()
    def request_set_default_target_configuration(self, req, product_id,
                                                 target):
        """
        @brief      Set the configuration of FBFUSE from the FBFUSE configuration server

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      target          A KATPOINT target string
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        try:
            target = Target(target)
        except Exception as error:
            return ("fail", str(error))
        if not product.capturing:
            return (
                "fail",
                "Product must be capturing before a target confiugration can be set."
            )
        product.reset_beams()
        # TBD: Here we connect to some database and request the default configurations
        # For example this may return secondary target in the FoV
        #
        # As a default the current system will put one beam directly on target and
        # the rest of the beams in a static tiling pattern around this target
        now = time.time()
        nbeams = product._beam_manager.nbeams
        product.add_tiling(target, nbeams - 1, 1.4e9, 0.5, now)
        product.add_beam(target)
        return ("ok", )

    @request(Str(), Str())
    @return_reply()
    def request_set_default_sb_configuration(self, req, product_id, sb_id):
        """
        @brief      Set the configuration of FBFUSE from the FBFUSE configuration server

        @param      product_id      This is a name for the data product, used to track which subarray is being deconfigured.
                                    For example "array_1_bc856M4k".

        @param      sb_id           The schedule block ID. Decisions of the configuarion of FBFUSE will be made dependent on
                                    the configuration of the current subarray, the primary and secondary science projects
                                    active and the targets expected to be visted during the execution of the schedule block.
        """
        try:
            product = self._get_product(product_id)
        except ProductLookupError as error:
            return ("fail", str(error))
        if product.capturing:
            return ("fail",
                    "Cannot reconfigure a currently capturing instance.")
        product.configure_coherent_beams(400, product._katpoint_antennas, 1,
                                         16)
        product.configure_incoherent_beam(product._katpoint_antennas, 1, 16)
        now = time.time()
        nbeams = product._beam_manager.nbeams
        product.add_tiling(target, nbeams - 1, 1.4e9, 0.5, now)
        product.add_beam(target)
        return ("ok", )
Example #20
0
 def test_unpack_types_many_with_multiple(self):
     expected = ['one', 2, 3]
     self.check_unpacking([Str(), Int(multiple=True)], [b'one', b'2', b'3'],
                          expected)
Example #21
0
class KATCPServer (DeviceServer):

  VERSION_INFO = ("ptuse-api", 1, 0)
  BUILD_INFO = ("ptuse-implementation", 0, 1, "")

  # Optionally set the KATCP protocol version and features. Defaults to
  # the latest implemented version of KATCP, with all supported optional
  # features
  PROTOCOL_INFO = ProtocolFlags(5, 0, set([
    ProtocolFlags.MULTI_CLIENT,
    ProtocolFlags.MESSAGE_IDS,
  ]))

  def __init__ (self, server_host, server_port, script):
    self.script = script
    self._host_sensors = {}
    self._beam_sensors = {}
    self._data_product = {}
    self._data_product["id"] = "None"

    self.data_product_res = []
    self.data_product_res.append(re.compile ("^[a-zA-Z]+_1"))
    self.data_product_res.append(re.compile ("^[a-zA-Z]+_2"))
    self.data_product_res.append(re.compile ("^[a-zA-Z]+_3"))
    self.data_product_res.append(re.compile ("^[a-zA-Z]+_4"))

    self.script.log(2, "KATCPServer::__init__ starting DeviceServer on " + server_host + ":" + str(server_port))
    DeviceServer.__init__(self, server_host, server_port)

  DEVICE_STATUSES = ["ok", "degraded", "fail"]

  def setup_sensors(self):
    """Setup server sensors."""
    self.script.log(2, "KATCPServer::setup_sensors()")

    self._device_status = Sensor.discrete("device-status",
      description="Status of entire system",
      params=self.DEVICE_STATUSES,
      default="ok")
    self.add_sensor(self._device_status)

    self._beam_name = Sensor.string("beam-name",
      description="name of configured beam",
      unit="",
      default="")
    self.add_sensor(self._beam_name)

    # setup host based sensors   
    self._host_name = Sensor.string("host-name",
      description="hostname of this server",
      unit="",
      default="")
    self.add_sensor(self._host_name)

    self.script.log(2, "KATCPServer::setup_sensors lmc="+str(self.script.lmc))
    (host, port) = self.script.lmc.split(":")
    self.setup_sensors_host (host, port)

    self.script.log(2, "KATCPServer::setup_sensors beams="+str(self.script.beam))
    self.setup_sensors_beam (self.script.beam_name)

  # add sensors based on the reply from the specified host
  def setup_sensors_host (self, host, port):

    self.script.log(2, "KATCPServer::setup_sensors_host ("+host+","+port+")")
    sock = sockets.openSocket (DL, host, int(port), 1)

    if sock:
      self.script.log(2, "KATCPServer::setup_sensors_host sock.send(" + self.script.lmc_cmd + ")") 
      sock.send (self.script.lmc_cmd + "\r\n")
      lmc_reply = sock.recv (65536)
      sock.close()
      xml = xmltodict.parse(lmc_reply)

      self._host_sensors = {}

      # Disk sensors
      self.script.log(2, "KATCPServer::setup_sensors_host configuring disk sensors")
      disk_prefix = host+".disk"
      self._host_sensors["disk_size"] = Sensor.float(disk_prefix+".size",
        description=host+": disk size",
        unit="MB",
        params=[8192,1e9],
        default=0)
      self._host_sensors["disk_available"] = Sensor.float(disk_prefix+".available",
        description=host+": disk available space",
        unit="MB",
        params=[1024,1e9],
        default=0)
      self.add_sensor(self._host_sensors["disk_size"])
      self.add_sensor(self._host_sensors["disk_available"])

      # Server Load sensors
      self.script.log(2, "KATCPServer::setup_sensors_host configuring load sensors")
      self._host_sensors["num_cores"] = Sensor.integer (host+".num_cores",
        description=host+": disk available space",
        unit="MB",
        params=[1,64],
        default=0)

      self._host_sensors["load1"] = Sensor.float(host+".load.1min",
        description=host+": 1 minute load ",
        unit="",
        default=0)

      self._host_sensors["load5"] = Sensor.float(host+".load.5min",
        description=host+": 5 minute load ",
        unit="",
        default=0)
      
      self._host_sensors["load15"] = Sensor.float(host+".load.15min",
        description=host+": 15 minute load ",
        unit="",
        default=0)

      self.add_sensor(self._host_sensors["num_cores"])
      self.add_sensor(self._host_sensors["load1"])
      self.add_sensor(self._host_sensors["load5"])
      self.add_sensor(self._host_sensors["load15"])

      cpu_temp_pattern  = re.compile("cpu[0-9]+_temp")
      fan_speed_pattern = re.compile("fan[0-9,a-z]+")
      power_supply_pattern = re.compile("ps[0-9]+_status")
        
      self.script.log(2, "KATCPServer::setup_sensors_host configuring other metrics")

      if not xml["lmc_reply"]["sensors"] == None:

        for sensor in xml["lmc_reply"]["sensors"]["metric"]:
          name = sensor["@name"]
          if name == "system_temp":
            self._host_sensors[name] = Sensor.float((host+".system_temp"),
              description=host+": system temperature",
              unit="C",
              params=[-20,150],
              default=0)
            self.add_sensor(self._host_sensors[name])

          if cpu_temp_pattern.match(name):
            (cpu, junk) = name.split("_")
            self._host_sensors[name] = Sensor.float((host+"." + name),
              description=host+": "+ cpu +" temperature",
              unit="C",
              params=[-20,150],
              default=0)
            self.add_sensor(self._host_sensors[name])

          if fan_speed_pattern.match(name):
            self._host_sensors[name] = Sensor.float((host+"." + name),
              description=host+": "+name+" speed",
              unit="RPM",
              params=[0,20000],
              default=0)
            self.add_sensor(self._host_sensors[name])

          if power_supply_pattern.match(name):
            self._host_sensors[name] = Sensor.boolean((host+"." + name),
              description=host+": "+name,
              unit="",
              default=0)
            self.add_sensor(self._host_sensors[name])

          # TODO consider adding power supply sensors: e.g.
          #   device-status-kronos1-powersupply1
          #   device-status-kronos1-powersupply2
          #   device-status-kronos2-powersupply1
          #   device-status-kronos2-powersupply2

          # TODO consider adding raid/disk sensors: e.g.
          #   device-status-<host>-raid
          #   device-status-<host>-raid-disk1
          #   device-status-<host>-raid-disk2

        self.script.log(2, "KATCPServer::setup_sensors_host done!")

      else:
        self.script.log(2, "KATCPServer::setup_sensors_host no sensors found")

    else:
      self.script.log(-2, "KATCPServer::setup_sensors_host: could not connect to LMC")

  # setup sensors for each beam
  def setup_sensors_beam (self, beam):

    b = str(beam)
    self._beam_sensors = {}

    self.script.log(2, "KATCPServer::setup_sensors_beam ="+b)

    self._beam_sensors["observing"] = Sensor.boolean("observing",
      description="Beam " + b + " is observing",
      unit="",
      default=0)
    self.add_sensor(self._beam_sensors["observing"])

    self._beam_sensors["snr"] = Sensor.float("snr",
      description="SNR of Beam "+b,
      unit="",
      params=[0,1e9],
      default=0)
    self.add_sensor(self._beam_sensors["snr"])

    self._beam_sensors["power"] = Sensor.float("power",
      description="Power Level of Beam "+b,
      unit="",
      default=0)
    self.add_sensor(self._beam_sensors["power"])

    self._beam_sensors["integrated"] = Sensor.float("integrated",
      description="Length of integration for Beam "+b,
      unit="",
      default=0)
    self.add_sensor(self._beam_sensors["integrated"])

  @request()
  @return_reply(Str())
  def request_beam(self, req):
    """Return the configure beam name."""
    return ("ok", self._beam_name.value())

  @request()
  @return_reply(Str())
  def request_host_name(self, req):
    """Return the name of this server."""
    return ("ok", self._host_name.value())

  @request()
  @return_reply(Float())
  def request_snr(self, req):
    """Return the SNR for this beam."""
    return ("ok", self._beam_sensors["snr"].value())

  @request()
  @return_reply(Float())
  def request_power(self, req):
    """Return the standard deviation of the 8-bit power level."""
    return ("ok", self._beam_sensors["power"].value())

  @request(Str(), Float())
  @return_reply(Str())
  def request_sync_time (self, req, data_product_id, adc_sync_time):
    """Set the ADC_SYNC_TIME for beam of the specified data product."""
    if not data_product_id == self._data_product["id"]:
      return ("fail", "data product " + str (data_product_id) + " was not configured")
    self.script.beam_config["lock"].acquire()
    self.script.beam_config["ADC_SYNC_TIME"] = str(adc_sync_time)
    self.script.beam_config["lock"].release()
    return ("ok", "")

  @request(Str(), Str())
  @return_reply(Str())
  def request_target_start (self, req, data_product_id, target_name):
    """Commence data processing on specific data product and beam using target."""
    self.script.log (1, "request_target_start(" + data_product_id + "," + target_name+")")

    self.script.beam_config["lock"].acquire()
    self.script.beam_config["ADC_SYNC_TIME"] = self.script.cam_config["ADC_SYNC_TIME"]
    self.script.beam_config["OBSERVER"] = self.script.cam_config["OBSERVER"]
    self.script.beam_config["ANTENNAE"] = self.script.cam_config["ANTENNAE"]
    self.script.beam_config["SCHEDULE_BLOCK_ID"] = self.script.cam_config["SCHEDULE_BLOCK_ID"]
    self.script.beam_config["EXPERIMENT_ID"] = self.script.cam_config["EXPERIMENT_ID"]
    self.script.beam_config["DESCRIPTION"] = self.script.cam_config["DESCRIPTION"]
    self.script.beam_config["lock"].release()

    # check the pulsar specified is listed in the catalog
    (result, message) = self.test_pulsar_valid (target_name)
    if result != "ok":
      return (result, message)

    # check the ADC_SYNC_TIME is valid for this beam
    if self.script.beam_config["ADC_SYNC_TIME"] == "0":
      return ("fail", "ADC Synchronisation Time was not valid")
  
    # set the pulsar name, this should include a check if the pulsar is in the catalog
    self.script.beam_config["lock"].acquire()
    if self.script.beam_config["MODE"] == "CAL":
      target_name = target_name + "_R"
    self.script.beam_config["SOURCE"] = target_name
    self.script.beam_config["lock"].release()

    host = self.script.tcs_host
    port = self.script.tcs_port

    self.script.log (2, "request_target_start: opening socket for beam " + beam_id + " to " + host + ":" + str(port))
    sock = sockets.openSocket (DL, host, int(port), 1)
    if sock:
      xml = self.script.get_xml_config()
      sock.send(xml + "\r\n")
      reply = sock.recv (65536)

      xml = self.script.get_xml_start_cmd()
      sock.send(xml + "\r\n")
      reply = sock.recv (65536)

      sock.close()
      return ("ok", "")
    else:
      return ("fail", "could not connect to TCS")

  @request(Str())
  @return_reply(Str())
  def request_target_stop (self, req, data_product_id):
    """Cease data processing with target_name."""
    self.script.log (1, "request_target_stop(" + data_product_id+")")

    self.script.beam_config["lock"].acquire()
    self.script.beam_config["SOURCE"] = ""
    self.script.beam_config["lock"].release()

    host = self.script.tcs_host
    port = self.script.tcs_port
    sock = sockets.openSocket (DL, host, int(port), 1)
    if sock:
      xml = self.script.get_xml_stop_cmd ()
      sock.send(xml + "\r\n")
      reply = sock.recv (65536)
      sock.close()
      return ("ok", "")
    else:
      return ("fail", "could not connect to tcs[beam]")

  @request(Str())
  @return_reply(Str())
  def request_capture_init (self, req, data_product_id):
    """Prepare the ingest process for data capture."""
    self.script.log (1, "request_capture_init: " + str(data_product_id) )
    if not data_product_id == self._data_product["id"]:
      return ("fail", "data product " + str (data_product_id) + " was not configured")
    return ("ok", "")

  @request(Str())
  @return_reply(Str())
  def request_capture_done(self, req, data_product_id):
    """Terminte the ingest process for the specified data_product_id."""
    self.script.log (1, "request_capture_done: " + str(data_product_id))
    if not data_product_id == self._data_product["id"]:
      return ("fail", "data product " + str (data_product_id) + " was not configured")
    return ("ok", "")

  @return_reply(Str())
  def request_configure(self, req, msg):
    """Prepare and configure for the reception of the data_product_id."""
    self.script.log (1, "request_configure: nargs= " + str(len(msg.arguments)) + " msg=" + str(msg))
    if len(msg.arguments) == 0:
      self.script.log (-1, "request_configure: no arguments provided")
      return ("ok", "configured data products: TBD")

    # the sub-array identifier
    data_product_id = msg.arguments[0]

    if len(msg.arguments) == 1:
      self.script.log (1, "request_configure: request for configuration of " + str(data_product_id))
      if data_product_id == self._data_product["id"]:
        configuration = str(data_product_id) + " " + \
                        str(self._data_product['antennas']) + " " + \
                        str(self._data_product['n_channels']) + " " + \
                        str(self._data_product['cbf_source'])
        self.script.log (1, "request_configure: configuration of " + str(data_product_id) + "=" + configuration)
        return ("ok", configuration)
      else:
        self.script.log (-1, "request_configure: no configuration existed for " + str(data_product_id))
        return ("fail", "no configuration existed for " + str(data_product_id))

    if len(msg.arguments) == 4:
      # if the configuration for the specified data product matches extactly the 
      # previous specification for that data product, then no action is required
      self.script.log (1, "configure: configuring " + str(data_product_id))

      if data_product_id == self._data_product["id"] and \
          self._data_product['antennas'] == msg.arguments[1] and \
          self._data_product['n_channels'] == msg.arguments[2] and \
          self._data_product['cbf_source'] == msg.arguments[3]:
        response = "configuration for " + str(data_product_id) + " matched previous"
        self.script.log (1, "configure: " + response)
        return ("ok", response)

      # the data product requires configuration
      else:
        self.script.log (1, "configure: new data product " + data_product_id)

        # determine which sub-array we are matched against
        the_sub_array = -1
        for i in range(4):
          self.script.log (1, "configure: testing self.data_product_res[" + str(i) +"].match(" + data_product_id +")")
          if self.data_product_res[i].match (data_product_id):
            the_sub_array = i + 1

        if the_sub_array == -1:
          self.script.log (1, "configure: could not match subarray from " + data_product_id)
          return ("fail", "could not data product to sub array")

        self.script.log (1, "configure: restarting pubsub for subarray " + str(the_sub_array))
        self.script.pubsub.set_sub_array (the_sub_array, self.script.beam_name)
        self.script.pubsub.restart()

        antennas = msg.arguments[1]
        n_channels = msg.arguments[2]
        cbf_source = msg.arguments[3]

        # check if the number of existing + new beams > available
        (cfreq, bwd, nchan) = self.script.cfg["SUBBAND_CONFIG_0"].split(":")
        if nchan != n_channels:
          self._data_product.pop(data_product_id, None)
          response = "PTUSE configured for " + nchan + " channels"
          self.script.log (-1, "configure: " + response)
          return ("fail", response)

        self._data_product['id'] = data_product_id
        self._data_product['antennas'] = antennas
        self._data_product['n_channels'] = n_channels
        self._data_product['cbf_source'] = cbf_source

        # parse the CBF_SOURCE to determine multicast groups
        (addr, port) = cbf_source.split(":")
        (mcast, count) = addr.split("+")

        self.script.log (2, "configure: parsed " + mcast + "+" + count + ":" + port)
        if not count == "1":
          response = "CBF source did not match ip_address+1:port"
          self.script.log (-1, "configure: " + response)
          return ("fail", response)

        mcasts = ["",""]
        ports = [0, 0]

        quartets = mcast.split(".")
        mcasts[0] = ".".join(quartets)
        quartets[3] = str(int(quartets[3])+1)
        mcasts[1] = ".".join(quartets)

        ports[0] = int(port)
        ports[1] = int(port)

        self.script.log (1, "configure: connecting to RECV instance to update configuration")

        for istream in range(int(self.script.cfg["NUM_STREAM"])):
          (host, beam_idx, subband) = self.script.cfg["STREAM_" + str(istream)].split(":")
          beam = self.script.cfg["BEAM_" + beam_idx]
          if beam == self.script.beam_name:

            # reset ADC_SYNC_TIME on the beam
            self.script.beam_config["lock"].acquire()
            self.script.beam_config["ADC_SYNC_TIME"] = "0";
            self.script.beam_config["lock"].release()

            port = int(self.script.cfg["STREAM_RECV_PORT"]) + istream
            self.script.log (3, "configure: connecting to " + host + ":" + str(port))
            sock = sockets.openSocket (DL, host, port, 1)
            if sock:
              req =  "<?req version='1.0' encoding='ISO-8859-1'?>"
              req += "<recv_cmd>"
              req +=   "<command>configure</command>"
              req +=   "<params>"
              req +=     "<param key='DATA_MCAST_0'>" + mcasts[0] + "</param>"
              req +=     "<param key='DATA_MCAST_1'>" + mcasts[1] + "</param>"
              req +=     "<param key='DATA_PORT_0'>" + str(ports[0]) + "</param>"
              req +=     "<param key='DATA_PORT_1'>" + str(ports[1]) + "</param>"
              req +=     "<param key='META_MCAST_0'>" + mcasts[0] + "</param>"
              req +=     "<param key='META_MCAST_1'>" + mcasts[1] + "</param>"
              req +=     "<param key='META_PORT_0'>" + str(ports[0]) + "</param>"
              req +=     "<param key='META_PORT_1'>" + str(ports[1]) + "</param>"
              req +=   "</params>"
              req += "</recv_cmd>"

              self.script.log (1, "configure: sending XML req")
              sock.send(req)
              recv_reply = sock.recv (65536)
              self.script.log (1, "configure: received " + recv_reply)
              sock.close()

      return ("ok", "data product " + str (data_product_id) + " configured")

    else:
      response = "expected 0, 1 or 4 arguments"
      self.script.log (-1, "configure: " + response)
      return ("fail", response)

  @return_reply(Str())
  def request_deconfigure(self, req, msg):
    """Deconfigure for the data_product."""

    if len(msg.arguments) == 0:
      self.script.log (-1, "request_configure: no arguments provided")
      return ("fail", "expected 1 argument")

    # the sub-array identifier
    data_product_id = msg.arguments[0]

    self.script.log (1, "configure: deconfiguring " + str(data_product_id))

    # check if the data product was previously configured
    if not data_product_id == self._data_product["id"]:
      response = str(data_product_id) + " did not match configured data product [" + self._data_product["id"] + "]"
      self.script.log (-1, "configure: " + response)
      return ("fail", response)

    for istream in range(int(self.script.cfg["NUM_STREAM"])):
      (host, beam_idx, subband) = self.script.cfg["STREAM_" + str(istream)].split(":")
      if self.script.beam_name == self.script.cfg["BEAM_" + beam_idx]:

        # reset ADC_SYNC_TIME on the beam
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["ADC_SYNC_TIME"] = "0";
        self.script.beam_config["lock"].release()

        port = int(self.script.cfg["STREAM_RECV_PORT"]) + istream
        self.script.log (3, "configure: connecting to " + host + ":" + str(port))
        sock = sockets.openSocket (DL, host, port, 1)
        if sock:

          req =  "<?req version='1.0' encoding='ISO-8859-1'?>"
          req += "<recv_cmd>"
          req +=   "<command>deconfigure</command>"
          req += "</recv_cmd>"

          sock.send(req)
          recv_reply = sock.recv (65536)
          sock.close()

      # remove the data product
      self._data_product["id"] = "None"

    response = "data product " + str(data_product_id) + " deconfigured"
    self.script.log (1, "configure: " + response)
    return ("ok", response)

  @request(Int())
  @return_reply(Str())
  def request_output_channels (self, req, nchannels):
    """Set the number of output channels."""
    if not self.test_power_of_two (nchannels):
      return ("fail", "number of channels not a power of two")
    if nchannels < 64 or nchannels > 4096:
      return ("fail", "number of channels not within range 64 - 2048")
    self.script.beam_config["OUTNCHAN"] = str(nchannels)
    return ("ok", "")

  @request(Int())
  @return_reply(Str())
  def request_output_bins(self, req, nbin):
    """Set the number of output phase bins."""
    if not self.test_power_of_two(nbin):
      return ("fail", "nbin not a power of two")
    if nbin < 64 or nbin > 2048:
      return ("fail", "nbin not within range 64 - 2048")
    self.script.beam_config["OUTNBIN"] = str(nbin)
    return ("ok", "")

  @request(Int())
  @return_reply(Str())
  def request_output_tsubint (self, req, tsubint):
    """Set the length of output sub-integrations."""
    if tsubint < 10 or tsubint > 60:
      return ("fail", "length of output subints must be between 10 and 600 seconds")
    self.script.beam_config["OUTTSUBINT"] = str(tsubint)
    return ("ok", "")

  @request(Float())
  @return_reply(Str())
  def request_dm(self, req, dm):
    """Set the value of dispersion measure to be removed"""
    if dm < 0 or dm > 2000:
      return ("fail", "dm not within range 0 - 2000")
    self.script.beam_config["DM"] = str(dm)
    return ("ok", "")

  @request(Float())
  @return_reply(Str())
  def request_cal_freq(self, req, cal_freq):
    """Set the value of noise diode firing frequecny in Hz."""
    if cal_freq < 0 or cal_freq > 1000:
      return ("fail", "CAL freq not within range 0 - 1000")
    self.script.beam_config["CALFREQ"] = str(cal_freq)
    if cal_freq == 0:
      self.script.beam_config["MODE"] = "PSR"
    else:
      self.script.beam_config["MODE"] = "CAL"
    return ("ok", "")

  # test if a number is a power of two
  def test_power_of_two (self, num):
    return num > 0 and not (num & (num - 1))

  # test whether the specified target exists in the pulsar catalog
  def test_pulsar_valid (self, target):

    self.script.log (2, "test_pulsar_valid: get_psrcat_param (" + target + ", jname)")
    (reply, message) = self.get_psrcat_param (target, "jname")
    if reply != "ok":
      return (reply, message)

    self.script.log (2, "test_pulsar_valid: get_psrcat_param () reply=" + reply + " message=" + message)
    if message == target:
      return ("ok", "")
    else:
      return ("fail", "pulsar " + target + " did not exist in catalog")

  def get_psrcat_param (self, target, param):
    cmd = "psrcat -all " + target + " -c " + param + " -nohead -o short"
    rval, lines = self.script.system (cmd, 3)
    if rval != 0 or len(lines) <= 0:
      return ("fail", "could not use psrcat")

    if lines[0].startswith("WARNING"):
      return ("fail", "pulsar " + target_name + " did not exist in catalog")

    parts = lines[0].split()
    if len(parts) == 2 and parts[0] == "1":
      return ("ok", parts[1])
Example #22
0
 def test_unpack_types_more_types_than_args(self):
     expected = ['one', 2, True, None]
     self.check_unpacking(
         [Str(), Int(),
          Bool(default=True),
          Str(optional=True)], [b'one', b'2'], expected)
Example #23
0
class JsonStatusServer(AsyncDeviceServer):
    VERSION_INFO = ("reynard-eff-jsonstatusserver-api", 0, 1)
    BUILD_INFO = ("reynard-eff-jsonstatusserver-implementation", 0, 1, "rc1")

    def __init__(self,
                 server_host,
                 server_port,
                 mcast_group=JSON_STATUS_MCAST_GROUP,
                 mcast_port=JSON_STATUS_PORT,
                 parser=EFF_JSON_CONFIG,
                 dummy=False):
        self._mcast_group = mcast_group
        self._mcast_port = mcast_port
        self._parser = parser
        self._dummy = dummy
        if not dummy:
            self._catcher_thread = StatusCatcherThread()
        else:
            self._catcher_thread = None
        self._monitor = None
        self._updaters = {}
        self._controlled = set()
        super(JsonStatusServer, self).__init__(server_host, server_port)

    @coroutine
    def _update_sensors(self):
        log.debug("Updating sensor values")
        data = self._catcher_thread.data
        if data is None:
            log.warning("Catcher thread has not received any data yet")
            return
        for name, params in self._parser.items():
            if name in self._controlled:
                continue
            if "updater" in params:
                self._sensors[name].set_value(params["updater"](data))

    def start(self):
        """start the server"""
        super(JsonStatusServer, self).start()
        if not self._dummy:
            self._catcher_thread.start()
            self._monitor = PeriodicCallback(self._update_sensors,
                                             1000,
                                             io_loop=self.ioloop)
            self._monitor.start()

    def stop(self):
        """stop the server"""
        if not self._dummy:
            if self._monitor:
                self._monitor.stop()
            self._catcher_thread.stop()
        return super(JsonStatusServer, self).stop()

    @request()
    @return_reply(Str())
    def request_xml(self, req):
        """request an XML version of the status message"""
        def make_elem(parent, name, text):
            child = etree.Element(name)
            child.text = text
            parent.append(child)

        @coroutine
        def convert():
            try:
                root = etree.Element("TelescopeStatus",
                                     attrib={"timestamp": str(time.time())})
                for name, sensor in self._sensors.items():
                    child = etree.Element("TelStat")
                    make_elem(child, "Name", name)
                    make_elem(child, "Value", str(sensor.value()))
                    make_elem(child, "Status", str(sensor.status()))
                    make_elem(child, "Type", self._parser[name]["type"])
                    if "units" in self._parser[name]:
                        make_elem(child, "Units", self._parser[name]["units"])
                    root.append(child)
            except Exception as error:
                req.reply("ok", str(error))
            else:
                req.reply("ok", etree.tostring(root))

        self.ioloop.add_callback(convert)
        raise AsyncReply

    @request()
    @return_reply(Str())
    def request_json(self, req):
        """request an JSON version of the status message"""
        return ("ok", self.as_json())

    def as_json(self):
        """Convert status sensors to JSON object"""
        out = {}
        for name, sensor in self._sensors.items():
            out[name] = str(sensor.value())
        return json.dumps(out)

    @request(Str())
    @return_reply(Str())
    def request_sensor_control(self, req, name):
        """take control of a given sensor value"""
        if name not in self._sensors:
            return ("fail", "No sensor named '{0}'".format(name))
        else:
            self._controlled.add(name)
            return ("ok", "{0} under user control".format(name))

    @request()
    @return_reply(Str())
    def request_sensor_control_all(self, req):
        """take control of all sensors value"""
        for name, sensor in self._sensors.items():
            self._controlled.add(name)
        return ("ok",
                "{0} sensors under user control".format(len(self._controlled)))

    @request()
    @return_reply(Int())
    def request_sensor_list_controlled(self, req):
        """List all controlled sensors"""
        count = len(self._controlled)
        for name in list(self._controlled):
            req.inform("{0} -- {1}".format(name, self._sensors[name].value()))
        return ("ok", count)

    @request(Str())
    @return_reply(Str())
    def request_sensor_release(self, req, name):
        """release a sensor from user control"""
        if name not in self._sensors:
            return ("fail", "No sensor named '{0}'".format(name))
        else:
            self._controlled.remove(name)
            return ("ok", "{0} released from user control".format(name))

    @request()
    @return_reply(Str())
    def request_sensor_release_all(self, req):
        """take control of all sensors value"""
        self._controlled = set()
        return ("ok", "All sensors released")

    @request(Str(), Str())
    @return_reply(Str())
    def request_sensor_set(self, req, name, value):
        """Set the value of a sensor"""
        if name not in self._sensors:
            return ("fail", "No sensor named '{0}'".format(name))
        if name not in self._controlled:
            return ("fail", "Sensor '{0}' not under user control".format(name))
        try:
            param = self._parser[name]
            value = TYPE_CONVERTER[param["type"]](value)
            self._sensors[name].set_value(value)
        except Exception as error:
            return ("fail", str(error))
        else:
            return ("ok", "{0} set to {1}".format(name,
                                                  self._sensors[name].value()))

    def setup_sensors(self):
        """Set up basic monitoring sensors.
        """
        for name, params in self._parser.items():
            if params["type"] == "float":
                sensor = Sensor.float(name,
                                      description=params["description"],
                                      unit=params.get("units", None),
                                      default=params.get("default", 0.0),
                                      initial_status=Sensor.UNKNOWN)
            elif params["type"] == "string":
                sensor = Sensor.string(name,
                                       description=params["description"],
                                       default=params.get("default", ""),
                                       initial_status=Sensor.UNKNOWN)
            elif params["type"] == "int":
                sensor = Sensor.integer(name,
                                        description=params["description"],
                                        default=params.get("default", 0),
                                        unit=params.get("units", None),
                                        initial_status=Sensor.UNKNOWN)
            elif params["type"] == "bool":
                sensor = Sensor.boolean(name,
                                        description=params["description"],
                                        default=params.get("default", False),
                                        initial_status=Sensor.UNKNOWN)
            else:
                raise Exception("Unknown sensor type '{0}' requested".format(
                    params["type"]))
            self.add_sensor(sensor)
Example #24
0
class FbfWorkerServer(AsyncDeviceServer):
    VERSION_INFO = ("fbf-control-server-api", 0, 1)
    BUILD_INFO = ("fbf-control-server-implementation", 0, 1, "rc1")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]
    STATES = ["idle", "preparing", "ready", "starting", "capturing", "stopping", "error"]
    IDLE, PREPARING, READY, STARTING, CAPTURING, STOPPING, ERROR = STATES

    def __init__(self, ip, port, dummy=False):
        """
        @brief       Construct new FbfWorkerServer instance

        @params  ip       The interface address on which the server should listen
        @params  port     The port that the server should bind to
        @params  de_ip    The IP address of the delay engine server
        @params  de_port  The port number for the delay engine server

        """
        self._dc_ip = None
        self._dc_port = None
        self._delay_client = None
        self._delay_client = None
        self._delays = None
        self._dummy = dummy
        self._dada_input_key = 0xdada
        self._dada_coh_output_key = 0xcaca
        self._dada_incoh_output_key = 0xbaba
        super(FbfWorkerServer, self).__init__(ip,port)

    @coroutine
    def start(self):
        """Start FbfWorkerServer server"""
        super(FbfWorkerServer,self).start()

    @coroutine
    def stop(self):
        yield self.deregister()
        yield super(FbfWorkerServer,self).stop()

    def setup_sensors(self):
        """
        @brief    Set up monitoring sensors.

        Sensor list:
        - device-status
        - local-time-synced
        - fbf0-status
        - fbf1-status

        @note     The following sensors are made available on top of default sensors
                  implemented in AsynDeviceServer and its base classes.

                  device-status:      Reports the health status of the FBFUSE and associated devices:
                                      Among other things report HW failure, SW failure and observation failure.
        """
        self._device_status_sensor = Sensor.discrete(
            "device-status",
            description = "Health status of FbfWorkerServer instance",
            params = self.DEVICE_STATUSES,
            default = "ok",
            initial_status = Sensor.NOMINAL)
        self.add_sensor(self._device_status_sensor)

        self._state_sensor = LoggingSensor.discrete(
            "state",
            params = self.STATES,
            description = "The current state of this worker instance",
            default = self.IDLE,
            initial_status = Sensor.NOMINAL)
        self._state_sensor.set_logger(log)
        self.add_sensor(self._state_sensor)

        self._delay_client_sensor = Sensor.string(
            "delay-engine-server",
            description = "The address of the currently set delay engine",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._delay_client_sensor)

        self._antenna_capture_order_sensor = Sensor.string(
            "antenna-capture-order",
            description = "The order in which the worker will capture antennas internally",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._antenna_capture_order_sensor)

        self._mkrecv_header_sensor = Sensor.string(
            "mkrecv-header",
            description = "The MKRECV/DADA header used for configuring capture with MKRECV",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._mkrecv_header_sensor)

    @property
    def capturing(self):
        return self.state == self.CAPTURING

    @property
    def idle(self):
        return self.state == self.IDLE

    @property
    def starting(self):
        return self.state == self.STARTING

    @property
    def stopping(self):
        return self.state == self.STOPPING

    @property
    def ready(self):
        return self.state == self.READY

    @property
    def preparing(self):
        return self.state == self.PREPARING

    @property
    def error(self):
        return self.state == self.ERROR

    @property
    def state(self):
        return self._state_sensor.value()

    def _system_call_wrapper(self, cmd):
        log.debug("System call: '{}'".format(" ".join(cmd)))
        if self._dummy:
            log.debug("Server is running in dummy mode, system call will be ignored")
        else:
            check_call(cmd)

    def _determine_feng_capture_order(self, antenna_to_feng_id_map, coherent_beam_config, incoherent_beam_config):
        # Need to sort the f-engine IDs into 4 states
        # 1. Incoherent but not coherent
        # 2. Incoherent and coherent
        # 3. Coherent but not incoherent
        # 4. Neither coherent nor incoherent
        #
        # We must catch all antennas as even in case 4 the data is required for the
        # transient buffer.
        #
        # To make this split, we first create the three sets, coherent, incoherent and all.
        mapping = antenna_to_feng_id_map
        all_feng_ids = set(mapping.values())
        coherent_feng_ids = set(mapping[antenna] for antenna in parse_csv_antennas(coherent_beam_config['antennas']))
        incoherent_feng_ids = set(mapping[antenna] for antenna in parse_csv_antennas(incoherent_beam_config['antennas']))
        incoh_not_coh = incoherent_feng_ids.difference(coherent_feng_ids)
        incoh_and_coh = incoherent_feng_ids.intersection(coherent_feng_ids)
        coh_not_incoh = coherent_feng_ids.difference(incoherent_feng_ids)
        used_fengs = incoh_not_coh.union(incoh_and_coh).union(coh_not_incoh)
        unused_fengs = all_feng_ids.difference(used_fengs)
        # Output final order
        final_order = list(incoh_not_coh) + list(incoh_and_coh) + list(coh_not_incoh) + list(unused_fengs)
        start_of_incoherent_fengs = 0
        end_of_incoherent_fengs = len(incoh_not_coh) + len(incoh_and_coh)
        start_of_coherent_fengs = len(incoh_not_coh)
        end_of_coherent_fengs = len(incoh_not_coh) + len(incoh_and_coh) + len(coh_not_incoh)
        start_of_unused_fengs = end_of_coherent_fengs
        end_of_unused_fengs = len(all_feng_ids)
        info = {
            "order": final_order,
            "incoherent_span":(start_of_incoherent_fengs, end_of_incoherent_fengs),
            "coherent_span":(start_of_coherent_fengs, end_of_coherent_fengs),
            "unused_span":(start_of_unused_fengs, end_of_unused_fengs)
        }
        return info

    @request(Str(), Int(), Int(), Float(), Float(), Str(), Str(), Str(), Str(), Str(), Int())
    @return_reply()
    def request_prepare(self, req, feng_groups, nchans_per_group, chan0_idx, chan0_freq,
                        chan_bw, mcast_to_beam_map, feng_config, coherent_beam_config,
                        incoherent_beam_config, dc_ip, dc_port):
        """
        @brief      Prepare FBFUSE to receive and process data from a subarray

        @detail     REQUEST ?configure feng_groups, nchans_per_group, chan0_idx, chan0_freq,
                        chan_bw, mcast_to_beam_map, antenna_to_feng_id_map, coherent_beam_config,
                        incoherent_beam_config
                    Configure FBFUSE for the particular data products

        @param      req                 A katcp request object

        @param      feng_groups         The contiguous range of multicast groups to capture F-engine data from,
                                        the parameter is formatted in stream notation, e.g.: spead://239.11.1.150+3:7147

        @param      nchans_per_group    The number of frequency channels per multicast group

        @param      chan0_idx           The index of the first channel in the set of multicast groups

        @param      chan0_freq          The frequency in Hz of the first channel in the set of multicast groups

        @param      chan_bw             The channel bandwidth in Hz

        @param      mcast_to_beam_map   A JSON mapping between output multicast addresses and beam IDs. This is the sole
                                        authority for the number of beams that will be produced and their indexes. The map
                                        is in the form:

                                        @code
                                           {
                                              "spead://239.11.2.150:7147":"cfbf00001,cfbf00002,cfbf00003,cfbf00004",
                                              "spead://239.11.2.151:7147":"ifbf00001"
                                           }

        @param      feng_config    JSON dictionary containing general F-engine parameters.

                                        @code
                                           {
                                              'bandwidth': 856e6,
                                              'centre-frequency': 1200e6,
                                              'sideband': 'upper',
                                              'feng-antenna-map': {...},
                                              'sync-epoch': 12353524243.0,
                                              'nchans': 4096
                                           }

        @param      coherent_beam_config   A JSON object specifying the coherent beam configuration in the form:

                                           @code
                                              {
                                                'tscrunch':16,
                                                'fscrunch':1,
                                                'antennas':'m007,m008,m009'
                                              }
                                           @endcode

        @param      incoherent_beam_config  A JSON object specifying the incoherent beam configuration in the form:

                                           @code
                                              {
                                                'tscrunch':16,
                                                'fscrunch':1,
                                                'antennas':'m007,m008,m009'
                                              }
                                           @endcode

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """
        if not self.idle:
            return ("fail", "FBF worker not in IDLE state")

        log.info("Preparing worker server instance")
        try:
            feng_config = json.loads(feng_config)
        except Exception as error:
            return ("fail", "Unable to parse F-eng config with error: {}".format(str(error)))
        try:
            mcast_to_beam_map = json.loads(mcast_to_beam_map)
        except Exception as error:
            return ("fail", "Unable to parse multicast beam mapping with error: {}".format(str(error)))
        try:
            coherent_beam_config = json.loads(coherent_beam_config)
        except Exception as error:
            return ("fail", "Unable to parse coherent beam config with error: {}".format(str(error)))
        try:
            incoherent_beam_config = json.loads(incoherent_beam_config)
        except Exception as error:
            return ("fail", "Unable to parse incoherent beam config with error: {}".format(str(error)))

        @coroutine
        def configure():
            self._state_sensor.set_value(self.PREPARING)
            log.debug("Starting delay configuration server client")
            self._delay_client = KATCPClientResource(dict(
                name="delay-configuration-client",
                address=(dc_ip, dc_port),
                controlled=True))
            self._delay_client.start()

            log.debug("Determining F-engine capture order")
            feng_capture_order_info = self._determine_feng_capture_order(feng_config['feng-antenna-map'], coherent_beam_config,
                incoherent_beam_config)
            log.debug("Capture order info: {}".format(feng_capture_order_info))
            feng_to_antenna_map = {value:key for key,value in feng_config['feng-antenna-map'].items()}
            antenna_capture_order_csv = ",".join([feng_to_antenna_map[feng_id] for feng_id in feng_capture_order_info['order']])
            self._antenna_capture_order_sensor.set_value(antenna_capture_order_csv)

            log.debug("Parsing F-engines to capture: {}".format(feng_groups))
            capture_range = ip_range_from_stream(feng_groups)
            ngroups = capture_range.count
            partition_nchans = nchans_per_group * ngroups
            partition_bandwidth = partition_nchans * chan_bw
            npol = 2
            ndim = 2
            nbits = 8
            tsamp = 1.0 / (feng_config['bandwidth'] / feng_config['nchans'])
            sample_clock = feng_config['bandwidth'] * 2
            timestamp_step =  feng_config['nchans'] * 2 * 256 # WARNING: This is only valid in 4k mode
            frequency_ids = [chan0_idx+nchans_per_group*ii for ii in range(ngroups)] #WARNING: Assumes contigous groups
            mkrecv_config = {
                'frequency_mhz': (chan0_freq + feng_config['nchans']/2.0 * chan_bw) / 1e6,
                'bandwidth': partition_bandwidth,
                'tsamp_us': tsamp * 1e6,
                'bytes_per_second': partition_bandwidth * npol * ndim * nbits,
                'nchan': partition_nchans,
                'dada_key': self._dada_input_key,
                'nantennas': len(feng_capture_order_info['order']),
                'antennas_csv': antenna_capture_order_csv,
                'sync_epoch': feng_config['sync-epoch'],
                'sample_clock': sample_clock,
                'mcast_sources': ",".join([str(group) for group in capture_range]),
                'mcast_port': capture_range.port,
                'interface': "192.168.0.1",
                'timestamp_step': timestamp_step,
                'ordered_feng_ids_csv': ",".join(map(str, feng_capture_order_info['order'])),
                'frequency_partition_ids_csv': ",".join(map(str,frequency_ids))
            }
            mkrecv_header = make_mkrecv_header(mkrecv_config)
            self._mkrecv_header_sensor.set_value(mkrecv_header)
            log.info("Determined MKRECV configuration:\n{}".format(mkrecv_header))


            log.debug("Parsing beam to multicast mapping")
            incoherent_beam = None
            incoherent_beam_group = None
            coherent_beam_to_group_map = {}
            for group, beams in mcast_to_beam_map.items():
                for beam in beams.split(","):
                    if beam.startswith("cfbf"):
                        coherent_beam_to_group_map[beam] = group
                    if beam.startswith("ifbf"):
                        incoherent_beam = beam
                        incoherent_beam_group = group

            log.debug("Determined coherent beam to multicast mapping: {}".format(coherent_beam_to_group_map))
            if incoherent_beam:
                log.debug("Incoherent beam will be sent to: {}".format(incoherent_beam_group))
            else:
                log.debug("No incoherent beam specified")


            """
            Tasks:
                - compile kernels
                - create shared memory banks
            """
            # Compile beamformer
            # TBD

            # Need to come up with a good way to allocate keys for dada buffers

            # Create input DADA buffer
            log.debug("Creating dada buffer for input with key '{}'".format("%x"%self._dada_input_key))
            #self._system_call_wrapper(["dada_db","-k",self._dada_input_key,"-n","64","-l","-p"])

            # Create coherent beam output DADA buffer
            log.debug("Creating dada buffer for coherent beam output with key '{}'".format("%x"%self._dada_coh_output_key))
            #self._system_call_wrapper(["dada_db","-k",self._dada_coh_output_key,"-n","64","-l","-p"])

            # Create incoherent beam output DADA buffer
            log.debug("Creating dada buffer for incoherent beam output with key '{}'".format("%x"%self._dada_incoh_output_key))
            #self._system_call_wrapper(["dada_db","-k",self._dada_incoh_output_key,"-n","64","-l","-p"])

            # Create SPEAD transmitter for coherent beams
            # Call to MKSEND

            # Create SPEAD transmitter for incoherent beam
            # Call to MKSEND

            # Need to pass the delay buffer controller the F-engine capture order but only for the coherent beams
            cstart, cend = feng_capture_order_info['coherent_span']
            coherent_beam_feng_capture_order = feng_capture_order_info['order'][cstart:cend]
            coherent_beam_antenna_capture_order = [feng_to_antenna_map[idx] for idx in coherent_beam_feng_capture_order]


            # Start DelayBufferController instance
            # Here we are going to make the assumption that the server and processing all run in
            # one docker container that will be preallocated with the right CPU set, GPUs, memory
            # etc. This means that the configurations need to be unique by NUMA node... [Note: no
            # they don't, we can use the container IPC channel which isolates the IPC namespaces.]
            if not self._dummy:
                n_coherent_beams = len(coherent_beam_to_group_map)
                coherent_beam_antennas = parse_csv_antennas(coherent_beam_config['antennas'])
                self._delay_buffer_controller = DelayBufferController(self._delay_client,
                    coherent_beam_to_group_map.keys(),
                    coherent_beam_antenna_capture_order, 1)
                yield self._delay_buffer_controller.start()
            # Start beamformer instance
            # TBD

            # Define MKRECV configuration file

            # SPEAD receiver does not get started until a capture init call
            self._state_sensor.set_value(self.READY)
            req.reply("ok",)

        self.ioloop.add_callback(configure)
        raise AsyncReply

    @request(Str())
    @return_reply()
    def request_deconfigure(self, req):
        """
        @brief      Deconfigure the FBFUSE instance.

        @note       Deconfigure the FBFUSE instance. If FBFUSE uses katportalclient to get information
                    from CAM, then it should disconnect at this time.

        @param      req               A katcp request object

        @return     katcp reply object [[[ !deconfigure ok | (fail [error description]) ]]]
        """

        # Need to make sure everything is stopped
        # Call self.stop?

        # Need to delete all allocated DADA buffers:

        @coroutine
        def deconfigure():
            log.info("Destroying dada buffer for input with key '{}'".format(self._dada_input_key))
            self._system_call_wrapper(["dada_db","-k",self._dada_input_key,"-d"])
            log.info("Destroying dada buffer for coherent beam output with key '{}'".format(self._dada_coh_output_key))
            self._system_call_wrapper(["dada_db","-k",self._dada_coh_output_key,"-n","64","-l","-p"])
            log.info("Destroying dada buffer for incoherent beam output with key '{}'".format(self._dada_incoh_output_key))
            self._system_call_wrapper(["dada_db","-k",self._dada_coh_output_key,"-n","64","-l","-p"])
            log.info("Destroying delay buffer controller")
            del self._delay_buffer_controller
            self._delay_buffer_controller = None
            req.reply("ok",)

        self.ioloop.add_callback(deconfigure)
        raise AsyncReply

    @request(Str())
    @return_reply()
    def request_capture_start(self, req):
        """
        @brief      Prepare FBFUSE ingest process for data capture.

        @note       A successful return value indicates that FBFUSE is ready for data capture and
                    has sufficient resources available. An error will indicate that FBFUSE is not
                    in a position to accept data

        @param      req               A katcp request object


        @return     katcp reply object [[[ !capture-init ok | (fail [error description]) ]]]
        """
        if not self.ready:
            return ("fail", "FBF worker not in READY state")
        # Here we start MKRECV running into the input dada buffer
        self._mkrecv_ingest_proc = Popen(["mkrecv","--config",self._mkrecv_config_filename], stdout=PIPE, stderr=PIPE)
        return ("ok",)

    @request(Str())
    @return_reply()
    def request_capture_stop(self, req):
        """
        @brief      Terminate the FBFUSE ingest process for the particular FBFUSE instance

        @note       This writes out any remaining metadata, closes all files, terminates any remaining processes and
                    frees resources for the next data capture.

        @param      req               A katcp request object

        @param      product_id        This is a name for the data product, used to track which subarray is being told to stop capture.
                                      For example "array_1_bc856M4k".

        @return     katcp reply object [[[ !capture-done ok | (fail [error description]) ]]]
        """
        if not self.capturing and not self.error:
            return ("ok",)

        @coroutine
        def stop_mkrecv_capture():
            #send SIGTERM to MKRECV
            log.info("Sending SIGTERM to MKRECV process")
            self._mkrecv_ingest_proc.terminate()
            self._mkrecv_timeout = 10.0
            log.info("Waiting {} seconds for MKRECV to terminate...".format(self._mkrecv_timeout))
            now = time.time()
            while time.time()-now < self._mkrecv_timeout:
                retval = self._mkrecv_ingest_proc.poll()
                if retval is not None:
                    log.info("MKRECV returned a return value of {}".format(retval))
                    break
                else:
                    yield sleep(0.5)
            else:
                log.warning("MKRECV failed to terminate in alloted time")
                log.info("Killing MKRECV process")
                self._mkrecv_ingest_proc.kill()
            req.reply("ok",)
        self.ioloop.add_callback(self.stop_mkrecv_capture)
        raise AsyncReply
Example #25
0
class TestDevice(object):
    def __init__(self):
        self.sent_messages = []

    @request(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    @return_reply(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    def request_one(self, sock, i, d, b):
        if i == 3:
            return ("fail", "I failed!")
        if i == 5:
            return ("bananas", "This should never be sent")
        if i == 6:
            return ("ok", i, d, b, "extra parameter")
        if i == 9:
            # This actually gets put in the callback params automatically
            orig_msg = Message.request("one", "foo", "bar")
            self.finish_request_one(orig_msg, sock, i, d, b)
            raise AsyncReply()
        return ("ok", i, d, b)

    @send_reply(Int(min=1, max=10), Discrete(("on", "off")), Bool())
    def finish_request_one(self, msg, sock, i, d, b):
        return (sock, msg, "ok", i, d, b)

    def reply(self, sock, msg, orig_msg):
        self.sent_messages.append([sock, msg])

    @request(Int(min=1, max=3, default=2),
             Discrete(("on", "off"), default="off"), Bool(default=True))
    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_two(self, sock, i, d, b):
        return ("ok", i, d, b)

    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    @request(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_three(self, sock, i, d, b):
        return ("ok", i, d, b)

    @return_reply()
    @request()
    def request_four(self, sock):
        return ["ok"]

    @inform(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def inform_one(self, sock, i, d, b):
        pass

    @request(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_five(self, i, d, b):
        return ("ok", i, d, b)

    @return_reply(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    @request(Int(min=1, max=3), Discrete(("on", "off")), Bool())
    def request_six(self, i, d, b):
        return ("ok", i, d, b)

    @return_reply(Int(), Str())
    @request(Int(), include_msg=True)
    def request_seven(self, msg, i):
        return ("ok", i, msg.name)

    @return_reply(Int(), Str())
    @request(Int(), include_msg=True)
    def request_eight(self, sock, msg, i):
        return ("ok", i, msg.name)
Example #26
0
class FitsInterfaceServer(AsyncDeviceServer):
    """
    Class providing an interface between EDD processes and the
    Effelsberg FITS writer
    """
    VERSION_INFO = ("edd-fi-server-api", 1, 0)
    BUILD_INFO = ("edd-fi-server-implementation", 0, 1, "")
    DEVICE_STATUSES = ["ok", "degraded", "fail"]
    PROTOCOL_INFO = ProtocolFlags(
        5, 0, set([
            ProtocolFlags.MULTI_CLIENT,
            ProtocolFlags.MESSAGE_IDS,
        ]))

    def __init__(self, interface, port, capture_interface, capture_port, fw_ip,
                 fw_port):
        """
        @brief Initialization of the FitsInterfaceServer object

        @param  interface          Interface address to serve on
        @param  port               Port number to serve on
        @param  capture_interface  Interface to capture data on
        @param  capture_port       Port to capture data on from instruments
        @param  fw_ip              IP address of the FITS writer
        @param  fw_port            Port number to connected to on FITS writer
        """
        self._configured = False
        self._no_active_beams = None
        self._nchannels = None
        self._integ_time = None
        self._blank_phase = None
        self._capture_interface = capture_interface
        self._capture_port = capture_port
        self._fw_connection_manager = FitsWriterConnectionManager(
            fw_ip, fw_port)
        self._capture_thread = None
        self._shutdown = False
        super(FitsInterfaceServer, self).__init__(interface, port)

    def start(self):
        """
        @brief   Start the server
        """
        self._fw_connection_manager.start()
        super(FitsInterfaceServer, self).start()

    def stop(self):
        """
        @brief   Stop the server
        """
        self._shutdown = True
        self._stop_capture()
        self._fw_connection_manager.stop()
        super(FitsInterfaceServer, self).stop()

    @property
    def nbeams(self):
        return self._active_beams_sensor.value()

    @nbeams.setter
    def nbeams(self, value):
        self._active_beams_sensor.set_value(value)

    @property
    def nchannels(self):
        return self._nchannels_sensor.value()

    @nchannels.setter
    def nchannels(self, value):
        self._nchannels_sensor.set_value(value)

    @property
    def integration_time(self):
        return self._integration_time_sensor.value()

    @integration_time.setter
    def integration_time(self, value):
        self._integration_time_sensor.set_value(value)

    @property
    def nblank_phases(self):
        return self._nblank_phases_sensor.value()

    @nblank_phases.setter
    def nblank_phases(self, value):
        self._nblank_phases_sensor.set_value(value)

    def setup_sensors(self):
        """
        @brief   Setup monitoring sensors
        """
        self._device_status_sensor = Sensor.discrete(
            "device-status",
            description="Health status of FIServer",
            params=self.DEVICE_STATUSES,
            default="ok",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._device_status_sensor)
        self._active_beams_sensor = Sensor.float(
            "nbeams",
            description="Number of beams that are currently active",
            default=1,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._active_beams_sensor)
        self._nchannels_sensor = Sensor.float(
            "nchannels",
            description="Number of channels in each beam",
            default=1,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._nchannels_sensor)
        self._integration_time_sensor = Sensor.float(
            "integration-time",
            description="The integration time for each beam",
            default=1,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._integration_time_sensor)
        self._nblank_phases_sensor = Sensor.integer(
            "nblank-phases",
            description="The number of blank phases",
            default=1,
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._nblank_phases_sensor)

    def _stop_capture(self):
        if self._capture_thread:
            log.debug("Cleaning up capture thread")
            self._capture_thread.stop()
            self._capture_thread.join()
            self._capture_thread = None
            log.debug("Capture thread cleaned")

    @request(Int(), Int(), Int(), Int())
    @return_reply()
    def request_configure(self, req, beams, channels, int_time, blank_phases):
        """
        @brief    Configure the FITS interface server

        @param   beams          The number of beams expected
        @param   channels       The number of channels expected
        @param   int_time       The integration time (milliseconds int)
        @param   blank_phases   The number of blank phases (1-4)

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """

        message = ("nbeams={}, nchannels={}, integration_time={},"
                   " nblank_phases={}").format(beams, channels, int_time,
                                               blank_phases)
        log.info("Configuring FITS interface server with params: {}".format(
            message))
        self.nbeams = beams
        self.nchannels = channels
        self.integration_time = int_time
        self.nblank_phases = blank_phases
        self._fw_connection_manager.drop_connection()
        self._stop_capture()
        self._configured = True
        return ("ok", )

    @request()
    @return_reply()
    def request_start(self, req):
        """
        @brief    Start the FITS interface server capturing data

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """
        log.info("Received start request")
        if not self._configured:
            msg = "FITS interface server is not configured"
            log.error(msg)
            return ("fail", msg)
        try:
            fw_socket = self._fw_connection_manager.get_transmit_socket()
        except Exception as error:
            log.exception(str(error))
            return ("fail", str(error))
        log.info("Starting FITS interface capture")
        self._stop_capture()
        buffer_size = 4 * (self.nchannels + 2)
        handler = R2SpectrometerHandler(2, self.nchannels,
                                        self.integration_time,
                                        self.nblank_phases, fw_socket)
        self._capture_thread = CaptureData(self._capture_interface,
                                           self._capture_port, buffer_size,
                                           handler)
        self._capture_thread.start()
        return ("ok", )

    @request()
    @return_reply()
    def request_stop(self, req):
        """
        @brief    Stop the FITS interface server capturing data

        @return     katcp reply object [[[ !configure ok | (fail [error description]) ]]]
        """
        log.info("Received stop request")
        if not self._configured:
            msg = "FITS interface server is not configured"
            log.error(msg)
            return ("fail", msg)
        log.info("Stopping FITS interface capture")
        self._stop_capture()
        self._fw_connection_manager.drop_connection()
        return ("ok", )
Example #27
0
class ProxyProtocol(DeviceProtocol):
    @request(include_msg=True)
    @return_reply(Int(min=0))
    def request_device_list(self, reqmsg):
        """Return a list of devices aggregated by the proxy.

        Returns the list of devices a sequence of #device-list informs.

        Inform Arguments
        ----------------
        device : str
            Name of a device.

        Returns
        -------
        success : {'ok', 'fail'}
            Whether sending the list of devices succeeded.
        informs : int
            Number of #device-list informs sent.

        Examples
        --------
        ?device-list
        #device-list antenna
        #device-list enviro
        !device-list ok 2
        """
        for name in sorted(self.factory.devices):
            self.send_message(
                Message.inform("device-list", name,
                               self.factory.devices[name].TYPE))
        return "ok", len(self.factory.devices)

    def request_sensor_list(self, msg):
        """Request the list of sensors.

        The list of sensors is sent as a sequence of #sensor-list informs.

        Parameters
        ----------
        name : str or pattern, optional
            If the name is not a pattern, list just the sensor with the given name.
            A pattern starts and ends with a slash ('/') and uses the Python re
            module's regular expression syntax. All sensors whose names contain the
            pattern are listed.  The default is to list all sensors.

        Inform Arguments
        ----------------
        name : str
            The name of the sensor being described.
        description : str
            Description of the named sensor.
        units : str
            Units for the value of the named sensor.
        type : str
            Type of the named sensor.
        params : list of str, optional
            Additional sensor parameters (type dependent). For integer and float
            sensors the additional parameters are the minimum and maximum sensor
            value. For discrete sensors the additional parameters are the allowed
            values. For all other types no additional parameters are sent.

        Returns
        -------
        success : {'ok', 'fail'}
            Whether sending the sensor list succeeded.
        informs : int
            Number of #sensor-list inform messages sent.

        Examples
        --------
        ?sensor-list
        #sensor-list psu.voltage PSU\_voltage. V float 0.0 5.0
        #sensor-list cpu.status CPU\_status. \@ discrete on off error
        ...
        !sensor-list ok 5

        ?sensor-list /voltage/
        #sensor-list psu.voltage PSU\_voltage. V float 0.0 5.0
        #sensor-list cpu.voltage CPU\_voltage. V float 0.0 3.0
        !sensor-list ok 2

        ?sensor-list cpu.power.on
        #sensor-list cpu.power.on Whether\_CPU\_hase\_power. \@ boolean
        !sensor-list ok 1
        """
        # handle non-regex cases
        if not msg.arguments or not (msg.arguments[0].startswith("/")
                                     and msg.arguments[0].endswith("/")):
            return DeviceProtocol.request_sensor_list(self, msg)

        # handle regex
        name_re = re.compile(msg.arguments[0][1:-1])
        sensors = dict([(name, sensor)
                        for name, sensor in self.factory.sensors.iteritems()
                        if name_re.search(name)])

        for name, sensor in sorted(sensors.items(), key=lambda x: x[0]):
            self.send_message(
                Message.inform("sensor-list", name, sensor.description,
                               sensor.units, sensor.stype,
                               *sensor.formatted_params))

        return Message.reply(msg.name, "ok", len(sensors))

    def _send_all_sensors(self, filter=None):
        """ Sends all sensor values with given filter (None = all)
        """
        counter = [0]  # this has to be a list or an object, thanks to

        # python lexical scoping rules (we could not write count += 1
        # in a function)

        def device_ok((informs, reply), device):
            for inform in informs:
                inform.arguments[2] = device.name + '.' + \
                                      inform.arguments[2]
                if filter is None or re.match(filter, inform.arguments[2]):
                    self.send_message(inform)
                    counter[0] += 1

        def all_ok(_):
            self.send_message(
                Message.reply('sensor-value', 'ok', str(counter[0])))

        wait_for = []
        for device in self.factory.devices.itervalues():
            if device.state == device.SYNCED:
                d = device.send_request('sensor-value')
                d.addCallback(device_ok, device)
                wait_for.append(d)
            # otherwise we don't have the list of sensors, so we don't
            # send the message
        DeferredList(wait_for).addCallback(all_ok)
        for name, sensor in self.factory.sensors.iteritems():
            if not isinstance(sensor, ProxiedSensor):
                if filter is None or re.match(filter, name):
                    timestamp_ms, status, value = sensor.read_formatted()
                    counter[0] += 1
                    self.send_message(
                        Message.inform('sensor-value', timestamp_ms, "1", name,
                                       status, value))
Example #28
0
class KATCPServer(DeviceServer):

    VERSION_INFO = ("ptuse-api", 2, 0)
    BUILD_INFO = ("ptuse-implementation", 0, 1, "")

    # Optionally set the KATCP protocol version and features. Defaults to
    # the latest implemented version of KATCP, with all supported optional
    # features
    PROTOCOL_INFO = ProtocolFlags(
        5, 0, set([
            ProtocolFlags.MULTI_CLIENT,
            ProtocolFlags.MESSAGE_IDS,
        ]))

    def __init__(self, server_host, server_port, script):
        self.script = script
        self._host_sensors = {}
        self._beam_sensors = {}
        self._data_product = {}
        self._data_product["id"] = "None"
        self._data_product["state"] = "None"

        self.data_product_res = []
        self.data_product_res.append(re.compile("^[a-zA-Z]+_1"))
        self.data_product_res.append(re.compile("^[a-zA-Z]+_2"))
        self.data_product_res.append(re.compile("^[a-zA-Z]+_3"))
        self.data_product_res.append(re.compile("^[a-zA-Z]+_4"))

        self.script.log(
            2, "KATCPServer::__init__ starting DeviceServer on " +
            server_host + ":" + str(server_port))
        DeviceServer.__init__(self, server_host, server_port)

    DEVICE_STATUSES = ["ok", "degraded", "fail"]

    def setup_sensors(self):
        """Setup server sensors."""
        self.script.log(2, "KATCPServer::setup_sensors()")

        self._device_status = Sensor.discrete(
            "device-status",
            description="Status of entire system",
            params=self.DEVICE_STATUSES,
            default="ok")
        self.add_sensor(self._device_status)

        self._beam_name = Sensor.string("beam-name",
                                        description="name of configured beam",
                                        unit="",
                                        default="")
        self.add_sensor(self._beam_name)

        # setup host based sensors
        self._host_name = Sensor.string("host-name",
                                        description="hostname of this server",
                                        unit="",
                                        default="")
        self.add_sensor(self._host_name)

        # GUI URL TODO remove hardcoding
        guis = [{
            "title": "PTUSE Web Interface",
            "description": "Live Pulsar timing monitoring plots",
            "href": self.script.cfg["SPIP_ADDRESS"]
        }]
        encoded = json.dumps(guis)
        self._gui_urls = Sensor.string("gui-urls",
                                       description="PTUSE GUI URL",
                                       unit="",
                                       default=encoded)
        self.add_sensor(self._gui_urls)
        self._gui_urls.set_value(encoded)

        # give LMC some time to prepare the socket
        time.sleep(5)

        self.script.log(
            1, "KATCPServer::setup_sensors lmc=" + str(self.script.lmc))
        (host, port) = self.script.lmc.split(":")
        self.setup_sensors_host(host, port)

        self.script.log(
            2, "KATCPServer::setup_sensors beams=" + str(self.script.beam))
        self.setup_sensors_beam(self.script.beam_name)

    # add sensors based on the reply from the specified host
    def setup_sensors_host(self, host, port):

        self.script.log(
            1, "KATCPServer::setup_sensors_host (" + host + "," + port + ")")
        sock = sockets.openSocket(DL, host, int(port), 1)

        if sock:
            self.script.log(
                2, "KATCPServer::setup_sensors_host sock.send(" +
                self.script.lmc_cmd + ")")
            sock.send(self.script.lmc_cmd + "\r\n")
            lmc_reply = sock.recv(65536)
            sock.close()
            xml = xmltodict.parse(lmc_reply)
            self.script.log(
                2, "KATCPServer::setup_sensors_host sock.recv=" + str(xml))

            self._host_sensors = {}

            # Disk sensors
            self.script.log(
                2, "KATCPServer::setup_sensors_host configuring disk sensors")
            disk_prefix = host + ".disk"
            self._host_sensors["disk_size"] = Sensor.float(
                disk_prefix + ".size",
                description=host + ": disk size",
                unit="MB",
                params=[8192, 1e9],
                default=0)
            self._host_sensors["disk_available"] = Sensor.float(
                disk_prefix + ".available",
                description=host + ": disk available space",
                unit="MB",
                params=[1024, 1e9],
                default=0)
            self.add_sensor(self._host_sensors["disk_size"])
            self.add_sensor(self._host_sensors["disk_available"])

            # Server Load sensors
            self.script.log(
                2, "KATCPServer::setup_sensors_host configuring load sensors")
            self._host_sensors["num_cores"] = Sensor.integer(
                host + ".num_cores",
                description=host + ": disk available space",
                unit="MB",
                params=[1, 64],
                default=0)

            self._host_sensors["load1"] = Sensor.float(host + ".load.1min",
                                                       description=host +
                                                       ": 1 minute load ",
                                                       unit="",
                                                       default=0)

            self._host_sensors["load5"] = Sensor.float(host + ".load.5min",
                                                       description=host +
                                                       ": 5 minute load ",
                                                       unit="",
                                                       default=0)

            self._host_sensors["load15"] = Sensor.float(host + ".load.15min",
                                                        description=host +
                                                        ": 15 minute load ",
                                                        unit="",
                                                        default=0)

            self._host_sensors["local_time_synced"] = Sensor.boolean(
                "local_time_synced",
                description=host + ": NTP server synchronisation",
                unit="",
                default=0)

            self.add_sensor(self._host_sensors["num_cores"])
            self.add_sensor(self._host_sensors["num_cores"])
            self.add_sensor(self._host_sensors["load1"])
            self.add_sensor(self._host_sensors["load5"])
            self.add_sensor(self._host_sensors["load15"])
            self.add_sensor(self._host_sensors["local_time_synced"])

            cpu_temp_pattern = re.compile("cpu[0-9]+_temp")
            fan_speed_pattern = re.compile("fan[0-9,a-z]+")
            power_supply_pattern = re.compile("ps[0-9]+_status")

            self.script.log(
                2, "KATCPServer::setup_sensors_host configuring other metrics")

            if not xml["lmc_reply"]["sensors"] == None:

                for sensor in xml["lmc_reply"]["sensors"]["metric"]:
                    name = sensor["@name"]
                    if name == "system_temp":
                        self._host_sensors[name] = Sensor.float(
                            (host + ".system_temp"),
                            description=host + ": system temperature",
                            unit="C",
                            params=[-20, 150],
                            default=0)
                        self.add_sensor(self._host_sensors[name])

                    if cpu_temp_pattern.match(name):
                        (cpu, junk) = name.split("_")
                        self._host_sensors[name] = Sensor.float(
                            (host + "." + name),
                            description=host + ": " + cpu + " temperature",
                            unit="C",
                            params=[-20, 150],
                            default=0)
                        self.add_sensor(self._host_sensors[name])

                    if fan_speed_pattern.match(name):
                        self._host_sensors[name] = Sensor.float(
                            (host + "." + name),
                            description=host + ": " + name + " speed",
                            unit="RPM",
                            params=[0, 20000],
                            default=0)
                        self.add_sensor(self._host_sensors[name])

                    if power_supply_pattern.match(name):
                        self._host_sensors[name] = Sensor.boolean(
                            (host + "." + name),
                            description=host + ": " + name,
                            unit="",
                            default=0)
                        self.add_sensor(self._host_sensors[name])

                    # TODO consider adding power supply sensors: e.g.
                    #   device-status-kronos1-powersupply1
                    #   device-status-kronos1-powersupply2
                    #   device-status-kronos2-powersupply1
                    #   device-status-kronos2-powersupply2

                    # TODO consider adding raid/disk sensors: e.g.
                    #   device-status-<host>-raid
                    #   device-status-<host>-raid-disk1
                    #   device-status-<host>-raid-disk2

                self.script.log(2, "KATCPServer::setup_sensors_host done!")

            else:
                self.script.log(
                    2, "KATCPServer::setup_sensors_host no sensors found")

        else:
            self.script.log(
                -2,
                "KATCPServer::setup_sensors_host: could not connect to LMC")

    # setup sensors for each beam
    def setup_sensors_beam(self, beam):

        b = str(beam)
        self._beam_sensors = {}

        self.script.log(2, "KATCPServer::setup_sensors_beam beam=" + b)

        self._beam_sensors["observing"] = Sensor.boolean("observing",
                                                         description="Beam " +
                                                         b + " is observing",
                                                         unit="",
                                                         default=0)
        self.add_sensor(self._beam_sensors["observing"])

        self._beam_sensors["snr"] = Sensor.float("snr",
                                                 description="SNR of Beam " +
                                                 b,
                                                 unit="",
                                                 params=[0, 1e9],
                                                 default=0)
        self.add_sensor(self._beam_sensors["snr"])

        self._beam_sensors["beamformer_stddev_polh"] = Sensor.float(
            "beamformer_stddev_polh",
            description="Standard deviation of beam voltages for pol H",
            unit="",
            params=[0, 127],
            default=0)
        self.add_sensor(self._beam_sensors["beamformer_stddev_polh"])

        self._beam_sensors["beamformer_stddev_polv"] = Sensor.float(
            "beamformer_stddev_polv",
            description="Standard deviation of beam voltages for pol V",
            unit="",
            params=[0, 127],
            default=0)
        self.add_sensor(self._beam_sensors["beamformer_stddev_polv"])

        self._beam_sensors["integrated"] = Sensor.float(
            "integrated",
            description="Length of integration for Beam " + b,
            unit="",
            default=0)
        self.add_sensor(self._beam_sensors["integrated"])

        self._beam_sensors["input_channels"] = Sensor.integer(
            "input_channels",
            description="Number of configured input channels for Beam " + b,
            unit="",
            default=0)
        self.add_sensor(self._beam_sensors["input_channels"])

    @request()
    @return_reply(Str())
    def request_beam(self, req):
        """Return the configure beam name."""
        return ("ok", self._beam_name.value())

    @request()
    @return_reply(Str())
    def request_host_name(self, req):
        """Return the name of this server."""
        return ("ok", self._host_name.value())

    @request()
    @return_reply(Float())
    def request_snr(self, req):
        """Return the SNR for this beam."""
        return ("ok", self._beam_sensors["snr"].value())

    @request()
    @return_reply(Float())
    def request_beamformer_stddev_polh(self, req):
        """Return the standard deviation of the 8-bit power level of pol H."""
        return ("ok", self._beam_sensors["beamformer_stddev_polh"].value())

    @request()
    @return_reply(Float())
    def request_beamformer_stddev_polv(self, req):
        """Return the standard deviation of the 8-bit power level of pol V."""
        return ("ok", self._beam_sensors["beamformer_stddev_polv"].value())

    @request()
    @return_reply(Float())
    def request_local_time_synced(self, req):
        """Return the sychronisation with NTP time"""
        return ("ok", self._beam_sensors["local_time_synced"].value())

    @request(Float())
    @return_reply(Str())
    def request_sync_time(self, req, adc_sync_time):
        """Set the ADC_SYNC_TIME for beam of the data product."""
        if self._data_product["id"] == "None":
            return ("fail", "data product was not configured")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["ADC_SYNC_TIME"] = str(adc_sync_time)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Str())
    @return_reply(Str())
    def request_proposal_id(self, req, proposal_id):
        """Set the PROPOSAL_ID for the data product."""
        if self._data_product["id"] == "None":
            return ("fail", "data product was not configured")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["PROPOSAL_ID"] = proposal_id
        self.script.beam_config["lock"].release()
        return ("ok", "")

    # ensure state changes work
    def change_state(self, command):

        state = self._data_product["state"]

        reply = "ok"
        message = ""

        if command == "configure":
            if state != "unconfigured":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "configured"

        elif command == "capture_init":
            if state != "configured":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "ready"

        elif command == "target_start":
            if state != "ready":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "recording"

        elif command == "target_stop":
            if state != "recording":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "ready"

        elif command == "capture_done":
            if state != "ready" and state != "configured":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "configured"

        elif command == "deconfigure":
            if state != "configured":
                message = "received " + command + " command when in " + state + " state"
            else:
                new_state = "unconfigured"

        if message == "":
            self.script.log(
                1, "change_state: " + self._data_product["state"] + " -> " +
                new_state)
            self._data_product["state"] = new_state
        else:
            self.script.log(-1, "change_state: " + message)
            reply = "fail"

        return (reply, message)

    @request(Str())
    @return_reply(Str())
    def request_target_start(self, req, target_name):
        """Commence data processing using target."""
        self.script.log(1, "request_target_start(" + target_name + ")")
        if self._data_product["id"] == "None":
            return ("fail", "data product was not configured")

        self.script.log(
            1, "request_target_start ADC_SYNC_TIME=" +
            self.script.cam_config["ADC_SYNC_TIME"])

        self.script.beam_config["lock"].acquire()
        self.script.beam_config["TARGET"] = self.script.cam_config["TARGET"]
        if self.script.cam_config["ADC_SYNC_TIME"] != "0":
            self.script.beam_config["ADC_SYNC_TIME"] = self.script.cam_config[
                "ADC_SYNC_TIME"]

        self.script.beam_config["NCHAN_PER_STREAM"] = self.script.cam_config[
            "NCHAN_PER_STREAM"]
        self.script.beam_config[
            "PRECISETIME_FRACTION_POLV"] = self.script.cam_config[
                "PRECISETIME_FRACTION_POLV"]
        self.script.beam_config[
            "PRECISETIME_FRACTION_POLH"] = self.script.cam_config[
                "PRECISETIME_FRACTION_POLH"]
        self.script.beam_config[
            "PRECISETIME_UNCERTAINTY_POLV"] = self.script.cam_config[
                "PRECISETIME_UNCERTAINTY_POLV"]
        self.script.beam_config[
            "PRECISETIME_UNCERTAINTY_POLH"] = self.script.cam_config[
                "PRECISETIME_UNCERTAINTY_POLH"]
        self.script.beam_config["TFR_KTT_GNSS"] = self.script.cam_config[
            "TFR_KTT_GNSS"]

        self.script.beam_config["ITRF"] = self.script.cam_config["ITRF"]
        self.script.beam_config["OBSERVER"] = self.script.cam_config[
            "OBSERVER"]
        self.script.beam_config["ANTENNAE"] = self.script.cam_config[
            "ANTENNAE"]
        self.script.beam_config["SCHEDULE_BLOCK_ID"] = self.script.cam_config[
            "SCHEDULE_BLOCK_ID"]
        self.script.beam_config["PROPOSAL_ID"] = self.script.cam_config[
            "PROPOSAL_ID"]
        self.script.beam_config["EXPERIMENT_ID"] = self.script.cam_config[
            "EXPERIMENT_ID"]
        self.script.beam_config["DESCRIPTION"] = self.script.cam_config[
            "DESCRIPTION"]
        self.script.beam_config["lock"].release()

        # check the pulsar specified is listed in the catalog
        (result, message) = self.test_pulsar_valid(target_name)
        if result != "ok":
            return (result, message)

        # check the ADC_SYNC_TIME is valid for this beam
        if self.script.beam_config["ADC_SYNC_TIME"] == "0":
            return ("fail", "ADC Synchronisation Time was not valid")

        # change the state
        (result, message) = self.change_state("target_start")
        if result != "ok":
            self.script.log(-1,
                            "target_start: change_state failed: " + message)
            return (result, message)

        # set the pulsar name, this should include a check if the pulsar is in the catalog
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["SOURCE"] = target_name
        self.script.beam_config["lock"].release()

        host = self.script.tcs_host
        port = self.script.tcs_port

        self.script.log(
            2, "request_target_start: opening socket to " + host + ":" +
            str(port))
        sock = sockets.openSocket(DL, host, int(port), 1)
        if sock:
            xml = self.script.get_xml_config()
            self.script.log(2,
                            "request_target_start: get_xml_config=" + str(xml))
            sock.send(xml + "\r\n")
            reply = sock.recv(65536)
            self.script.log(2, "request_target_start: reply=" + str(reply))

            xml = self.script.get_xml_start_cmd()
            self.script.log(
                2, "request_target_start: get_xml_start_cmd=" + str(xml))
            sock.send(xml + "\r\n")
            reply = sock.recv(65536)
            self.script.log(2, "request_target_start: reply=" + str(reply))

            sock.close()
            return ("ok", "")
        else:
            return ("fail", "could not connect to TCS")

    @request()
    @return_reply(Str())
    def request_target_stop(self, req):
        """Cease data processing with target_name."""
        self.script.log(1, "request_target_stop()")
        return self.target_stop()

    def target_stop(self):

        if self._data_product["id"] == "None":
            return ("fail", "data product was not configured")

        # change the state
        (result, message) = self.change_state("target_stop")
        if result != "ok":
            self.script.log(-1, "target_stop: change_state failed: " + message)
            return (result, message)

        self.script.reset_beam_config()

        host = self.script.tcs_host
        port = self.script.tcs_port
        sock = sockets.openSocket(DL, host, int(port), 1)
        if sock:
            xml = self.script.get_xml_stop_cmd()
            sock.send(xml + "\r\n")
            reply = sock.recv(65536)
            sock.close()
            return ("ok", "")
        else:
            return ("fail", "could not connect to tcs[beam]")

    @request()
    @return_reply(Str())
    def request_capture_init(self, req):
        """Prepare the ingest process for data capture."""
        self.script.log(1, "request_capture_init()")

        # change the state
        (result, message) = self.change_state("capture_init")
        if result != "ok":
            self.script.log(-1,
                            "capture_init: change_state failed: " + message)
            return (result, message)

        return ("ok", "")

    @request()
    @return_reply(Str())
    def request_capture_done(self, req):
        """Terminte the ingest process."""
        self.script.log(1, "request_capture_done()")
        return self.capture_done()

    def capture_done(self):

        # in case the observing was terminated early
        if self._data_product["state"] == "recording":
            (result, message) = self.target_stop()

        # change the state
        (result, message) = self.change_state("capture_done")
        if result != "ok":
            self.script.log(-1,
                            "capture_done: change_state failed: " + message)
            return (result, message)

        return ("ok", "")

    @return_reply(Str())
    def request_configure(self, req, msg):
        """Prepare and configure for the reception of the data_product_id."""
        self.script.log(
            1, "request_configure: nargs= " + str(len(msg.arguments)) +
            " msg=" + str(msg))
        if len(msg.arguments) == 0:
            self.script.log(-1, "request_configure: no arguments provided")
            return ("ok", "configured data products: TBD")

        # the sub-array identifier
        data_product_id = msg.arguments[0]

        if len(msg.arguments) == 1:
            self.script.log(
                1, "request_configure: request for configuration of " +
                str(data_product_id))
            if data_product_id == self._data_product["id"]:
                configuration = str(data_product_id) + " " + \
                                str(self._data_product['antennas']) + " " + \
                                str(self._data_product['n_channels']) + " " + \
                                str(self._data_product['cbf_source']) + " " + \
                                str(self._data_product['proxy_name'])
                self.script.log(
                    1, "request_configure: configuration of " +
                    str(data_product_id) + "=" + configuration)
                return ("ok", configuration)
            else:
                self.script.log(
                    -1, "request_configure: no configuration existed for " +
                    str(data_product_id))
                return ("fail",
                        "no configuration existed for " + str(data_product_id))

        if len(msg.arguments) == 5:
            # if the configuration for the specified data product matches extactly the
            # previous specification for that data product, then no action is required
            self.script.log(1,
                            "configure: configuring " + str(data_product_id))

            if data_product_id == self._data_product["id"] and \
                self._data_product['antennas'] == msg.arguments[1] and \
                self._data_product['n_channels'] == msg.arguments[2] and \
                self._data_product['cbf_source'] == str(msg.arguments[3]) and \
                self._data_product['proxy_name'] == str(msg.arguments[4]):
                response = "configuration for " + str(
                    data_product_id) + " matched previous"
                self.script.log(1, "configure: " + response)
                return ("ok", response)

            # the data product requires configuration
            else:
                self.script.log(
                    1, "configure: new data product " + data_product_id)

                # TODO decide what to do regarding preconfigured params (e.g. FREQ, BW) vs CAM supplied values

                # determine which sub-array we are matched against
                the_sub_array = -1
                for i in range(4):
                    self.script.log(
                        1, "configure: testing self.data_product_res[" +
                        str(i) + "].match(" + data_product_id + ")")
                    if self.data_product_res[i].match(data_product_id):
                        the_sub_array = i + 1

                if the_sub_array == -1:
                    self.script.log(
                        1, "configure: could not match subarray from " +
                        data_product_id)
                    return ("fail", "could not data product to sub array")

                antennas = msg.arguments[1]
                n_channels = msg.arguments[2]
                cbf_source = str(msg.arguments[3])
                streams = json.loads(msg.arguments[3])
                proxy_name = str(msg.arguments[4])

                self.script.log(2, "configure: streams=" + str(streams))

                # check if the number of existing + new beams > available
                # (cfreq, bwd, nchan1) = self.script.cfg["SUBBAND_CONFIG_0"].split(":")
                # (cfreq, bwd, nchan2) = self.script.cfg["SUBBAND_CONFIG_1"].split(":")
                # nchan = int(nchan1) + int(nchan2)
                #if nchan != int(n_channels):
                #  self._data_product.pop(data_product_id, None)
                #  response = "PTUSE configured for " + str(nchan) + " channels"
                #  self.script.log (-1, "configure: " + response)
                #  return ("fail", response)

                self._data_product['id'] = data_product_id
                self._data_product['antennas'] = antennas
                self._data_product['n_channels'] = n_channels
                self._data_product['cbf_source'] = cbf_source
                self._data_product['streams'] = str(streams)
                self._data_product['proxy_name'] = proxy_name
                self._data_product['state'] = "unconfigured"

                # change the state
                (result, message) = self.change_state("configure")
                if result != "ok":
                    self.script.log(
                        -1, "configure: change_state failed: " + message)
                    return (result, message)

                # determine the CAM metadata server and update pubsub
                cam_server = "None"
                fengine_stream = "None"
                polh_stream = "None"
                polv_stream = "None"

                self.script.log(
                    2, "configure: streams.keys()=" + str(streams.keys()))
                self.script.log(
                    2, "configure: streams['cam.http'].keys()=" +
                    str(streams['cam.http'].keys()))

                if 'cam.http' in streams.keys(
                ) and 'camdata' in streams['cam.http'].keys():
                    cam_server = streams['cam.http']['camdata']
                    self.script.log(2,
                                    "configure: cam_server=" + str(cam_server))

                if 'cbf.antenna_channelised_voltage' in streams.keys():
                    stream_name = streams[
                        'cbf.antenna_channelised_voltage'].keys()[0]
                    fengine_stream = stream_name.split(".")[0]
                    self.script.log(
                        2, "configure: fengine_stream=" + str(fengine_stream))

                if 'cbf.tied_array_channelised_voltage' in streams.keys():
                    for s in streams[
                            'cbf.tied_array_channelised_voltage'].keys():
                        if s.endswith('y'):
                            polv_stream = s
                        if s.endswith('x'):
                            polh_stream = s
                    self.script.log(
                        2, "configure: polh_stream=" + str(polh_stream) +
                        " polv_stream=" + str(polv_stream))

                if cam_server != "None" and fengine_stream != "None" and polh_stream != "None":
                    self.script.pubsub.update_cam(cam_server, fengine_stream,
                                                  polh_stream, polv_stream,
                                                  antennas)
                else:
                    response = "Could not extract streams[cam.http][camdata]"
                    self.script.log(1, "configure: cam_server=" + cam_server)
                    self.script.log(
                        1, "configure: fengine_stream=" + fengine_stream)
                    self.script.log(1, "configure: polh_stream=" + polh_stream)
                    self.script.log(-1, "configure: " + response)
                    return ("fail", response)

                # restart the pubsub service
                self.script.log(
                    1, "configure: restarting pubsub for new meta-data")
                self.script.pubsub.restart()

                # determine the X and Y tied array channelised voltage streams
                mcasts = {}
                ports = {}
                key = 'cbf.tied_array_channelised_voltage'
                if key in streams.keys():
                    stream = 'i0.tied-array-channelised-voltage.0x'
                    if stream in streams[key].keys():
                        (mcast,
                         port) = self.parseStreamAddress(streams[key][stream])
                        mcasts['x'] = mcast
                        ports['x'] = int(port)
                    else:
                        response = "Could not extract streams[" + key + "][" + stream + "]"
                        self.script.log(-1, "configure: " + response)
                        return ("fail", response)

                    stream = 'i0.tied-array-channelised-voltage.0y'
                    if stream in streams[key].keys():
                        (mcast,
                         port) = self.parseStreamAddress(streams[key][stream])
                        mcasts['y'] = mcast
                        ports['y'] = int(port)
                    else:
                        response = "Could not extract streams[" + key + "][" + stream + "]"
                        self.script.log(-1, "configure: " + response)
                        return ("fail", response)

                # if the backend nchan is < CAM nchan
                self.script.log(1, "configure: n_channels=" + str(n_channels))
                if int(n_channels) == 1024 and False:
                    nchan = 992
                    self.script.log(
                        1, "configure: reconfiguring MCAST groups from  " +
                        mcasts['x'] + ", " + mcasts['y'])
                    (mcast_base_x, mcast_ngroups_x) = mcasts['x'].split("+")
                    (mcast_base_y, mcast_ngroups_y) = mcasts['y'].split("+")
                    nchan_per_group = int(n_channels) / int(mcast_ngroups_x)
                    new_ngroups = nchan / nchan_per_group
                    offset = (int(mcast_ngroups_x) - new_ngroups) / 2
                    self.script.log(
                        1, "configure: nchan_per_group=" +
                        str(nchan_per_group) + " new_ngroups=" +
                        str(new_ngroups) + " offset=" + str(offset))
                    parts_x = mcast_base_x.split(".")
                    parts_y = mcast_base_y.split(".")
                    parts_x[3] = str(int(parts_x[3]) + offset)
                    parts_y[3] = str(int(parts_y[3]) + offset)
                    mcasts['x'] = ".".join(parts_x) + "+" + str(new_ngroups)
                    mcasts['y'] = ".".join(parts_y) + "+" + str(new_ngroups)
                    self.script.log(
                        1, "configure: reconfigured MCAST groups to " +
                        mcasts['x'] + ", " + mcasts['y'])

                self.script.log(
                    1,
                    "configure: connecting to RECV instance to update configuration"
                )

                for istream in range(int(self.script.cfg["NUM_STREAM"])):
                    (host, beam_idx,
                     subband) = self.script.cfg["STREAM_" +
                                                str(istream)].split(":")
                    beam = self.script.cfg["BEAM_" + beam_idx]
                    self.script.log(
                        1, "configure: istream=" + str(istream) + " beam=" +
                        beam + " script.beam_name=" + self.script.beam_name)
                    if beam == self.script.beam_name:

                        # reset ADC_SYNC_TIME on the beam
                        self.script.beam_config["lock"].acquire()
                        self.script.beam_config["ADC_SYNC_TIME"] = "0"
                        self.script.beam_config["lock"].release()

                        port = int(
                            self.script.cfg["STREAM_RECV_PORT"]) + istream
                        self.script.log(
                            1, "configure: connecting to " + host + ":" +
                            str(port))
                        sock = sockets.openSocket(DL, host, port, 1)
                        if sock:
                            req = "<?req version='1.0' encoding='ISO-8859-1'?>"
                            req += "<recv_cmd>"
                            req += "<command>configure</command>"
                            req += "<params>"

                            req += "<param key='DATA_MCAST_0'>" + mcasts[
                                'x'] + "</param>"
                            req += "<param key='DATA_PORT_0'>" + str(
                                ports['x']) + "</param>"
                            req += "<param key='META_MCAST_0'>" + mcasts[
                                'x'] + "</param>"
                            req += "<param key='META_PORT_0'>" + str(
                                ports['x']) + "</param>"
                            req += "<param key='DATA_MCAST_1'>" + mcasts[
                                'y'] + "</param>"
                            req += "<param key='DATA_PORT_1'>" + str(
                                ports['y']) + "</param>"
                            req += "<param key='META_MCAST_1'>" + mcasts[
                                'y'] + "</param>"
                            req += "<param key='META_PORT_1'>" + str(
                                ports['y']) + "</param>"

                            req += "</params>"
                            req += "</recv_cmd>"

                            self.script.log(
                                1, "configure: sending XML req [" + req + "]")
                            sock.send(req)
                            self.script.log(
                                1, "configure: send XML, receiving reply")
                            recv_reply = sock.recv(65536)
                            self.script.log(
                                1, "configure: received " + recv_reply)
                            sock.close()
                        else:
                            response = "configure: could not connect to stream " + str(
                                istream) + " at " + host + ":" + str(port)
                            self.script.log(-1, "configure: " + response)
                            return ("fail", response)

            return ("ok",
                    "data product " + str(data_product_id) + " configured")

        else:
            response = "expected 0, 1 or 5 arguments, received " + str(
                len(msg.arguments))
            self.script.log(-1, "configure: " + response)
            return ("fail", response)

    # parse address of from spead://AAA.BBB.CCC.DDD+NN:PORT into
    def parseStreamAddress(self, stream):

        self.script.log(2, "parseStreamAddress: parsing " + stream)
        (prefix, spead_address) = stream.split("//")
        (mcast, port) = spead_address.split(":")
        self.script.log(2, "parseStreamAddress: parsed " + mcast + ":" + port)

        return (mcast, port)

    @return_reply(Str())
    def request_deconfigure(self, req, msg):
        """Deconfigure for the data_product."""
        self.script.log(1, "request_deconfigure()")

        # in case the observing was terminated early
        if self._data_product["state"] == "recording":
            (result, message) = self.target_stop()

        if self._data_product["state"] == "ready":
            (result, message) = self.capture_done()

        data_product_id = self._data_product["id"]

        # check if the data product was previously configured
        if not data_product_id == self._data_product["id"]:
            response = str(
                data_product_id
            ) + " did not match configured data product [" + self._data_product[
                "id"] + "]"
            self.script.log(-1, "configure: " + response)
            return ("fail", response)

        # change the state
        (result, message) = self.change_state("deconfigure")
        if result != "ok":
            self.script.log(-1, "deconfigure: change_state failed: " + message)
            return (result, message)

        for istream in range(int(self.script.cfg["NUM_STREAM"])):
            (host, beam_idx,
             subband) = self.script.cfg["STREAM_" + str(istream)].split(":")
            if self.script.beam_name == self.script.cfg["BEAM_" + beam_idx]:

                # reset ADC_SYNC_TIME on the beam
                self.script.beam_config["lock"].acquire()
                self.script.beam_config["ADC_SYNC_TIME"] = "0"
                self.script.beam_config["lock"].release()

                port = int(self.script.cfg["STREAM_RECV_PORT"]) + istream
                self.script.log(
                    3, "configure: connecting to " + host + ":" + str(port))
                sock = sockets.openSocket(DL, host, port, 1)
                if sock:

                    req = "<?req version='1.0' encoding='ISO-8859-1'?>"
                    req += "<recv_cmd>"
                    req += "<command>deconfigure</command>"
                    req += "</recv_cmd>"

                    sock.send(req)
                    recv_reply = sock.recv(65536)
                    sock.close()

            # remove the data product
            self._data_product["id"] = "None"

        response = "data product " + str(data_product_id) + " deconfigured"
        self.script.log(1, "configure: " + response)
        return ("ok", response)

    @request(Int())
    @return_reply(Str())
    def request_output_channels(self, req, nchannels):
        """Set the number of output channels."""
        self.script.log(1,
                        "request_output_channels: nchannels=" + str(nchannels))
        if not self.test_power_of_two(nchannels):
            self.script.log(
                -1, "request_output_channels: " + str(nchannels) +
                " not a power of two")
            return ("fail", "number of channels not a power of two")
        if nchannels < 64 or nchannels > 4096:
            self.script.log(
                -1, "request_output_channels: " + str(nchannels) +
                " not within range 64 - 4096")
            return ("fail", "number of channels not within range 64 - 4096")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTNCHAN"] = str(nchannels)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Int())
    @return_reply(Str())
    def request_output_bins(self, req, nbin):
        """Set the number of output phase bins."""
        self.script.log(1, "request_output_bins: nbin=" + str(nbin))
        if not self.test_power_of_two(nbin):
            self.script.log(
                -1,
                "request_output_bins: " + str(nbin) + " not a power of two")
            return ("fail", "nbin not a power of two")
        if nbin < 64 or nbin > 2048:
            self.script.log(
                -1, "request_output_bins: " + str(nbin) +
                " not within range 64 - 2048")
            return ("fail", "nbin not within range 64 - 2048")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTNBIN"] = str(nbin)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Int())
    @return_reply(Str())
    def request_output_tsubint(self, req, tsubint):
        """Set the length of output sub-integrations."""
        self.script.log(1, "request_output_tsubint: tsubint=" + str(tsubint))
        if tsubint < 10 or tsubint > 60:
            self.script.log(
                -1, "request_output_tsubint: " + str(tsubint) +
                " not within range 10 - 60")
            return (
                "fail",
                "length of output subints must be between 10 and 60 seconds")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTTSUBINT"] = str(tsubint)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Float())
    @return_reply(Str())
    def request_dispersion_measure(self, req, dm):
        """Set the value of dispersion measure to be removed"""
        self.script.log(1, "request_dispersion_measure: dm=" + str(dm))
        if dm > 2000:
            self.script.log(
                -1, "request_dispersion_measure: " + str(dm) + " > 2000")
            return ("fail", "dm greater than limit of 2000")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["DM"] = str(dm)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Float())
    @return_reply(Str())
    def request_calibration_freq(self, req, cal_freq):
        """Set the value of noise diode firing frequecny in Hz."""
        self.script.log(1,
                        "request_calibration_freq: cal_freq=" + str(cal_freq))
        if cal_freq < 0 or cal_freq > 1000:
            return ("fail", "CAL freq not within range 0 - 1000")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["CALFREQ"] = str(cal_freq)
        if cal_freq == 0:
            self.script.beam_config["MODE"] = "PSR"
        else:
            self.script.beam_config["MODE"] = "CAL"
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Int())
    @return_reply(Str())
    def request_output_npol(self, req, outnpol):
        """Set the number of output pol parameters."""
        self.script.log(1, "request_output_npol: outnpol=" + str(outnpol))
        if outnpol != 1 and outnpol != 2 and outnpol != 3 and outnpol != 4:
            self.script.log(
                -1, "request_output_npol: " + str(outnpol) + " not 1, 2 or 4")
            return ("fail", "output npol must be between 1, 2 or 4")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTNPOL"] = str(outnpol)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Int())
    @return_reply(Str())
    def request_output_nbit(self, req, outnbit):
        """Set the number of bits per output sample."""
        self.script.log(1, "request_output_nbit: outnbit=" + str(outnbit))
        if outnbit != 1 and outnbit != 2 and outnbit != 4 and outnbit != 8:
            self.script.log(
                -1,
                "request_output_nbit: " + str(outnbit) + " not 1, 2, 4 or 8")
            return ("fail", "output nbit must be between 1, 2, 4 or 8")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTNBIT"] = str(outnbit)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request(Int())
    @return_reply(Str())
    def request_output_tdec(self, req, outtdec):
        """Set the number of input samples integrated into 1 output sample."""
        self.script.log(1, "request_output_tdec: outtdec=" + str(outtdec))
        if outtdec < 16 or outtdec > 131072:
            self.script.log(
                -1, "request_output_tdec: " + str(outtdec) +
                " not in range [16..131072]")
            return ("fail", "output tdec must be between 16 and 131072")
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["OUTTDEC"] = str(outtdec)
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request()
    @return_reply(Str())
    def request_fold_mode(self, req):
        """Set the processing mode to produce folded archives."""
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["PERFORM_FOLD"] = "1"
        self.script.beam_config["PERFORM_SEARCH"] = "0"
        self.script.log(1, "request_search_mode: PERFORM_FOLD=1")
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request()
    @return_reply(Str())
    def request_search_mode(self, req):
        """Set the processing mode to produce filterbank data."""
        self.script.beam_config["lock"].acquire()
        self.script.beam_config["PERFORM_FOLD"] = "0"
        self.script.beam_config["PERFORM_SEARCH"] = "1"
        self.script.log(1, "request_search_mode: PERFORM_SEARCH=1")
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request()
    @return_reply(Str())
    def request_disable_zeroed_buffers(self, req):
        """Disable zeroing of ring buffers, enabling stats mode."""
        self.script.beam_config["lock"].acquire()
        self.script.log(1, "request_disable_zeroed_buffers: ZERO_COPY=0")
        self.script.beam_config["ZERO_COPY"] = "0"
        self.script.beam_config["lock"].release()
        return ("ok", "")

    @request()
    @return_reply(Str())
    def request_enable_zeroed_buffers(self, req):
        """Enable zeroing of ring buffers, disabling stats mode."""
        self.script.beam_config["lock"].acquire()
        self.script.log(1, "request_enable_zeroed_buffers: ZERO_COPY=1")
        self.script.beam_config["ZERO_COPY"] = "1"
        self.script.beam_config["lock"].release()
        return ("ok", "")

    # test if a number is a power of two
    def test_power_of_two(self, num):
        return num > 0 and not (num & (num - 1))

    # test whether the specified target exists in the pulsar catalog
    def test_pulsar_valid(self, target):

        self.script.log(2, "test_pulsar_valid: target='[" + target + "]")

        # remove the _R suffix
        if target.endswith('_R'):
            target = target[:-2]

        # check if the target matches the fluxcal.on file
        cmd = "grep " + target + " " + self.script.cfg[
            "CONFIG_DIR"] + "/fluxcal.on | wc -l"
        rval, lines = self.script.system(cmd, 3)
        if rval == 0 and len(lines) == 1 and int(lines[0]) > 0:
            return ("ok", "")

        # check if the target matches the fluxcal.off file
        cmd = "grep " + target + " " + self.script.cfg[
            "CONFIG_DIR"] + "/fluxcal.off | wc -l"
        rval, lines = self.script.system(cmd, 3)
        if rval == 0 and len(lines) == 1 and int(lines[0]) > 0:
            return ("ok", "")

        self.script.log(
            2, "test_pulsar_valid: get_psrcat_param (" + target + ", jname)")
        (reply, message) = self.get_psrcat_param(target, "jname")
        if reply != "ok":
            return (reply, message)

        self.script.log(
            2, "test_pulsar_valid: get_psrcat_param () reply=" + reply +
            " message=" + message)
        if message == target:
            return ("ok", "")
        else:
            return ("fail", "pulsar " + target + " did not exist in catalog")

    def get_psrcat_param(self, target, param):

        # remove the _R suffix
        if target.endswith('_R'):
            target = target[:-2]

        cmd = "psrcat -all " + target + " -c " + param + " -nohead -o short"
        rval, lines = self.script.system(cmd, 3)
        if rval != 0 or len(lines) <= 0:
            return ("fail", "could not use psrcat")

        if lines[0].startswith("WARNING"):
            return ("fail",
                    "pulsar " + target_name + " did not exist in catalog")

        parts = lines[0].split()
        if len(parts) == 2 and parts[0] == "1":
            return ("ok", parts[1])