Пример #1
0
    def test_mutable_nested_tree_external_change(self, test_tree_mutable):

        new_tree = ParameterTree({
            'immutable_param': "Hello",
            "tree": test_tree_mutable.param_tree
        })

        new_node = {"new": 65}
        path = 'tree/extra'
        test_tree_mutable.param_tree.set('extra', new_node)
        val = new_tree.get(path)
        assert val['extra'] == new_node
Пример #2
0
class SystemInfo(with_metaclass(Singleton, object)):
    """SystemInfo - class that extracts and stores information about system-level parameters."""

    # __metaclass__ = Singleton

    def __init__(self):
        """Initialise the SystemInfo object.

        This constructor initlialises the SystemInfo object, extracting various system-level
        parameters and storing them in a parameter tree to be accessible to clients.
        """
        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Extract platform information and store in parameter tree
        (system, node, release, version, machine, processor) = platform.uname()
        platform_tree = ParameterTree({
            'system': system,
            'node': node,
            'release': release,
            'version': version,
            'processor': processor
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version':
            version_info['version'],
            'tornado_version':
            tornado.version,
            'platform':
            platform_tree,
            'server_uptime': (self.get_server_uptime, None),
        })

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the SystemInfo adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)
Пример #3
0
class SystemInfo(with_metaclass(Singleton, object)):
    """SystemInfo - class that extracts and stores information about system-level parameters."""

    # __metaclass__ = Singleton

    def __init__(self):
        """Initialise the SystemInfo object.

        This constructor initlialises the SystemInfo object, extracting various system-level
        parameters and storing them in a parameter tree to be accessible to clients.
        """
        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Extract platform information and store in parameter tree
        (system, node, release, version, machine, processor) = platform.uname()
        platform_tree = ParameterTree({
            'system': system,
            'node': node,
            'release': release,
            'version': version,
            'processor': processor
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version': version_info['version'],
            'tornado_version': tornado.version,
            'platform': platform_tree,
            'server_uptime': (self.get_server_uptime, None),
        })

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the SystemInfo adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)
Пример #4
0
    def test_mutable_nested_tree_in_immutable_tree(self, test_tree_mutable):

        new_tree = ParameterTree({
            'immutable_param': "Hello",
            "nest": {
                "tree": test_tree_mutable.param_tree
            }
        })

        new_node = {"new": 65}
        path = 'nest/tree/extra'
        new_tree.set(path, new_node)
        val = new_tree.get(path)
        assert val['extra'] == new_node
Пример #5
0
    def test_mutable_nested_tree_delete(self, test_tree_mutable):

        new_tree = ParameterTree({
            'immutable_param': "Hello",
            "tree": test_tree_mutable.param_tree
        })

        path = 'tree/bonus'
        new_tree.delete(path)

        tree = new_tree.get('')

        assert 'bonus' not in tree['tree']

        with pytest.raises(ParameterTreeError) as excinfo:
            test_tree_mutable.param_tree.get(path)

        assert "Invalid path" in str(excinfo.value)
Пример #6
0
class ExcaliburDetector(object):
    """EXCALIBUR detector class.

    This class implements the representation of an EXCALIBUR detector, providing the composition
    of one or more ExcaliburFem instances into a complete detector.
    """

    ALL_FEMS = 0
    ALL_CHIPS = 0

    def __init__(self, fem_connections):
        """Initialise the ExcaliburDetector object.

        :param fem_connections: list of (address, port) FEM connections to make
        """

        self.num_pending = 0
        self.command_succeeded = True

        self.api_trace = False
        self.state_lock = threading.Lock()

        self.powercard_fem_idx = None
        self.chip_enable_mask = None

        self.fe_param_map = ExcaliburFrontEndParameterMap()

        self.fe_param_read = {
            'param': ['none'],
            'fem': -1,
            'chip': -1,
            'value': {},
        }
        self.fe_param_write = [{
            'param': 'none',
            'fem': -1,
            'chip': -1,
            'offset': 0,
            'value': [],
        }]

        self.fems = []
        if not isinstance(fem_connections, (list, tuple)):
            fem_connections = [fem_connections]
        if isinstance(fem_connections, tuple) and len(fem_connections) >= 2:
            fem_connections = [fem_connections]

        try:
            fem_id = 1
            for connection in fem_connections:
                (host_addr, port) = connection[:2]
                data_addr = connection[2] if len(connection) > 2 else None

                self.fems.append(
                    ExcaliburDetectorFemConnection(fem_id, host_addr,
                                                   int(port), data_addr))
                fem_id += 1
        except Exception as e:
            raise ExcaliburDetectorError(
                'Failed to initialise detector FEM list: {}'.format(e))

        self._fem_thread_pool = futures.ThreadPoolExecutor(
            max_workers=len(self.fems))

        self.fe_cmd_map = ExcaliburFrontEndCommandMap()

        cmd_tree = OrderedDict()
        cmd_tree['api_trace'] = (self._get('api_trace'), self.set_api_trace)
        cmd_tree['connect'] = (None, self.connect)
        cmd_tree.update(self.fe_cmd_map)
        for cmd in self.fe_cmd_map:
            cmd_tree[cmd] = (None, partial(self.do_command, cmd))
        cmd_tree['fe_param_read'] = (self._get('fe_param_read'),
                                     self.read_fe_param)
        cmd_tree['fe_param_write'] = (self._get('fe_param_write'),
                                      self.write_fe_param)

        self.param_tree = ParameterTree({
            'status': {
                'connected': (self.connected, None),
                'command_pending': (self.command_pending, None),
                'command_succeeded': (self._get('command_succeeded'), None),
                'num_pending': (self._get('num_pending'), None),
                'fem': [fem.param_tree for fem in self.fems],
                'powercard_fem_idx': (self._get('powercard_fem_idx'), None),
            },
            'command': cmd_tree,
        })

    def set_powercard_fem_idx(self, idx):

        if idx < -1 or idx > len(self.fems):
            raise ExcaliburDetectorError(
                'Illegal FEM index {} specified for power card'.format(idx))

        self.powercard_fem_idx = idx

    def set_chip_enable_mask(self, chip_enable_mask):

        if not isinstance(chip_enable_mask, (list, tuple)):
            chip_enable_mask = [chip_enable_mask]

        if len(chip_enable_mask) != len(self.fems):
            raise ExcaliburDetectorError(
                'Mismatch in length of asic enable mask ({}) versus number of FEMS ({})'
                .format(len(chip_enable_mask), len(self.fems)))

        for (fem_idx, mask) in enumerate(chip_enable_mask):
            self.fems[fem_idx].set_chip_enable(mask)

        self.chip_enable_mask = chip_enable_mask

    def set_fem_timeout(self, fem_timeout_ms):

        for fem_idx in range(len(self.fems)):
            self.fems[fem_idx].timeout_ms = fem_timeout_ms

    def get(self, path):

        try:
            return self.param_tree.get(path)
        except ParameterTreeError as e:
            raise ExcaliburDetectorError(e)

    def set(self, path, data):

        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise ExcaliburDetectorError(e)

    def _get(self, attr):

        return lambda: getattr(self, attr)

    def _increment_pending(self):

        with self.state_lock:
            self.num_pending += 1

    def _decrement_pending(self, success):

        with self.state_lock:
            self.num_pending -= 1
            if not success:
                self.command_succeeded = False

    def _set_fem_error_state(self, fem_idx, error_code, error_msg):

        with self.state_lock:
            self.fems[fem_idx].error_code = error_code
            self.fems[fem_idx].error_msg = error_msg

    def connected(self):

        with self.state_lock:
            connected = all([fem.connected for fem in self.fems])

        return connected

    def command_pending(self):

        with self.state_lock:
            command_pending = self.num_pending > 0

        return command_pending

    def set_api_trace(self, params):

        trace = False
        if 'enabled' in params:
            trace = params['enabled']

        self.command_succeeded = True
        if not isinstance(trace, bool):
            raise ExcaliburDetectorError(
                'api_trace requires a bool enabled parameter')

        if trace != self.api_trace:
            self.api_trace = bool(trace)
            for idx in range(len(self.fems)):
                self.fems[idx].fem.set_api_trace(trace)

    def connect(self, params):

        state = True
        if 'state' in params:
            state = params['state']

        self.command_succeeded = True
        """Establish connection to the detectors FEMs."""
        for idx in range(len(self.fems)):
            self._increment_pending()
            if state:
                self._connect_fem(idx)
            else:
                self._disconnect_fem(idx)

    @run_on_executor(executor='_fem_thread_pool')
    def _connect_fem(self, idx):

        connect_ok = True

        logging.debug(
            'Connecting FEM {} at {}:{}, data_addr {} timeout {} ms'.format(
                self.fems[idx].fem_id, self.fems[idx].host_addr,
                self.fems[idx].port, self.fems[idx].data_addr,
                self.fems[idx].timeout_ms))

        try:
            self.fems[idx].fem = ExcaliburFem(self.fems[idx].fem_id,
                                              self.fems[idx].host_addr,
                                              self.fems[idx].port,
                                              self.fems[idx].data_addr,
                                              self.fems[idx].timeout_ms)

            self.fems[
                idx].state = ExcaliburDetectorFemConnection.STATE_CONNECTED
            self.fems[idx].connected = True
            self._set_fem_error_state(idx, FEM_RTN_OK, '')
            logging.debug('Connected FEM {}'.format(self.fems[idx].fem_id))

        except ExcaliburFemError as e:
            self.fems[idx].state = ExcaliburDetectorFemConnection.STATE_ERROR
            self.fems[idx].connected = False
            self.fems[idx].error_code = FEM_RTN_INTERNALERROR
            self.fems[idx].error_msg = str(e)
            logging.error('Failed to connect to FEM {} at {}:{}: {}'.format(
                self.fems[idx].fem_id, self.fems[idx].host_addr,
                self.fems[idx].port, str(e)))
            connect_ok = False

        else:

            # Set up chip enables according to asic enable mask
            try:
                (param_id, _, _, _,
                 _) = self.fe_param_map['medipix_chip_disable']
                for chip_idx in range(CHIPS_PER_FEM):
                    chip_disable = 0 if chip_idx + 1 in self.fems[
                        idx].chips_enabled else 1
                    rc = self.fems[idx].fem.set_int(chip_idx + 1, param_id, 0,
                                                    chip_disable)
                    if rc != FEM_RTN_OK:
                        self.fems[idx].error_msg = self.fems[
                            idx].fem.get_error_msg()
                        logging.error(
                            'FEM {}: chip {} enable set returned error {}: {}'.
                            format(self.fems[idx].fem_id, rc,
                                   self.fems[idx].error_msg))
            except Exception as e:
                self.fems[idx].error_code = FEM_RTN_INTERNALERROR
                self.fems[
                    idx].error_msg = 'Unable to build chip enable list for FEM {}: {}'.format(
                        idx, e)
                logging.error(self.fems[idx].error_msg)
                connect_ok = False

        self._decrement_pending(connect_ok)

    @run_on_executor(executor='_fem_thread_pool')
    def _disconnect_fem(self, idx):

        disconnect_ok = self._do_disconnect(idx)
        self._decrement_pending(disconnect_ok)

    def _do_disconnect(self, idx):

        disconnect_ok = True

        logging.debug("Disconnecting from FEM {}".format(
            self.fems[idx].fem_id))

        try:
            if self.fems[idx].fem is not None:
                self.fems[idx].fem.close()
        except ExcaliburFemError as e:
            logging.error("Failed to disconnect from FEM {}: {}".format(
                self.fems[idx].fem_id, str(e)))
            disconnect_ok = False

        self.fems[idx].connected = False
        self.fems[
            idx].state = ExcaliburDetectorFemConnection.STATE_DISCONNECTED

        return disconnect_ok

    def _cmd(self, cmd_name):

        return partial(self.do_command, cmd_name)

    def _build_fem_idx_list(self, fem_ids):

        if not isinstance(fem_ids, list):
            fem_ids = [fem_ids]

        if fem_ids == [ExcaliburDetector.ALL_FEMS]:
            fem_idx_list = range(len(self.fems))
        else:
            fem_idx_list = [fem_id - 1 for fem_id in fem_ids]

        return fem_idx_list

    def do_command(self, cmd_name, params):

        logging.debug('{} called with params {}'.format(cmd_name, params))

        fem_idx_list = range(len(self.fems))
        chip_ids = [self.ALL_CHIPS]

        if params is not None:

            if 'fem' in params:
                fem_idx_list = self._build_fem_idx_list(params['fem'])

            if 'chip' in params:
                chip_ids = params['chip']

        self.command_succeeded = True

        for idx in fem_idx_list:
            self._increment_pending()
            self._do_command(idx, chip_ids, *self.fe_cmd_map[cmd_name])

    @run_on_executor(executor='_fem_thread_pool')
    def _do_command(self, fem_idx, chip_ids, cmd_id, cmd_text, param_err):

        logging.debug(
            "FEM {} chip {}: {} command (id={}) in thread {:x}".format(
                self.fems[fem_idx].fem_id, chip_ids, cmd_text, cmd_id,
                threading.current_thread().ident))

        self._set_fem_error_state(fem_idx, FEM_RTN_OK, '')
        cmd_ok = True

        if not self.fems[fem_idx].connected:
            self._set_fem_error_state(
                fem_idx, FEM_RTN_CONNECTION_CLOSED,
                "{} command failed: FEM is not connected".format(cmd_text))
            logging.error("FEM %s: %s", self.fems[fem_idx].fem_id,
                          self.fems[fem_idx].error_msg)
            cmd_ok = False
        else:
            for chip_id in chip_ids:
                try:
                    rc = self.fems[fem_idx].fem.cmd(chip_id, cmd_id)
                    if rc != FEM_RTN_OK:
                        self._set_fem_error_state(
                            fem_idx, rc,
                            self.fems[fem_idx].fem.get_error_msg())
                        logging.error(
                            "FEM {}: {} command returned error {}: {}".format(
                                self.fems[fem_idx].fem_id, cmd_text, rc,
                                self.fems[fem_idx].error_msg))
                        if rc in [FEM_RTN_CONNECTION_CLOSED, FEM_RTN_TIMEOUT]:
                            self._do_disconnect(fem_idx)
                        cmd_ok = False

                except ExcaliburFemError as e:
                    self._set_fem_error_state(fem_idx, param_err, str(e))
                    logging.error(
                        "FEM {}: {} command raised exception: {}".format(
                            self.fems[fem_idx].fem_id, cmd_text, str(e)))
                    cmd_ok = False

        self._decrement_pending(cmd_ok)

    def read_fe_param(self, attrs):

        logging.debug("In read_fe_param with attrs {} thread id {}".format(
            attrs,
            threading.current_thread().ident))

        self.command_succeeded = True
        required_attrs = ('param', 'fem', 'chip')
        for attr in required_attrs:
            try:
                self.fe_param_read[attr] = attrs[attr]
            except KeyError:
                self.command_succeeded = False
                raise ExcaliburDetectorError(
                    'Frontend parameter read command is missing {} attribute'.
                    format(attr))

        if isinstance(attrs['param'], list):
            params = attrs['param']
        else:
            params = [attrs['param']]

        for param in params:

            if param not in self.fe_param_map:
                self.command_succeeded = False
                raise ExcaliburDetectorError(
                    'Frontend parameter read - illegal parameter name {}'.
                    format(param))

        fem_idx_list = self._build_fem_idx_list(attrs['fem'])

        for param in params:
            self.fe_param_read['value'][param] = [[]
                                                  for fem_idx in fem_idx_list]

        for (res_idx, fem_idx) in enumerate(fem_idx_list):
            self._increment_pending()
            self._read_fe_param(fem_idx, attrs['chip'], params, res_idx)

        logging.debug('read_fe_param returning')

    @run_on_executor(executor='_fem_thread_pool')
    def _read_fe_param(self, fem_idx, chips, params, res_idx):

        logging.debug("FEM {}: _read_fe_param in thread {:x}".format(
            self.fems[fem_idx].fem_id,
            threading.current_thread().ident))

        self._set_fem_error_state(fem_idx, FEM_RTN_OK, '')
        read_ok = True

        try:
            for param in params:

                (param_id, param_type, param_read_len, param_mode,
                 param_access) = self.fe_param_map[param]

                try:
                    fem_get_method = getattr(self.fems[fem_idx].fem,
                                             'get_' + param_type)

                except AttributeError:
                    self._set_fem_error_state(
                        fem_idx, FEM_RTN_INTERNALERROR,
                        'Read of frontend parameter {} failed: cannot resolve read method'
                        .format(param))
                    read_ok = False

                else:
                    if param_mode == ParamPerChip:
                        if chips == ExcaliburDetector.ALL_CHIPS:
                            chip_list = self.fems[fem_idx].chips_enabled
                        else:
                            chip_list = [chips]
                    else:
                        chip_list = [0]

                    values = []
                    for chip in chip_list:
                        (rc, value) = fem_get_method(chip, param_id,
                                                     param_read_len)
                        if rc != FEM_RTN_OK:
                            self._set_fem_error_state(
                                fem_idx, rc,
                                self.fems[fem_idx].fem.get_error_msg())

                            value = [-1]
                            read_ok = False

                            if rc in [
                                    FEM_RTN_CONNECTION_CLOSED, FEM_RTN_TIMEOUT
                            ]:
                                self._do_disconnect(fem_idx)

                        values.extend(value)
                        if not read_ok:
                            break

                    values = values[0] if len(values) == 1 else values
                    with self.state_lock:
                        self.fe_param_read['value'][param][res_idx] = values

        except Exception as e:
            self._set_fem_error_state(
                fem_idx, FEM_RTN_INTERNALERROR,
                'Read of frontend parameter {} failed: {}'.format(param, e))
            read_ok = False

        if not read_ok:
            logging.error('FEM {}: {}'.format(self.fems[fem_idx].fem_id,
                                              self.fems[fem_idx].error_msg))

        self._decrement_pending(read_ok)

    def write_fe_param(self, params):

        logging.debug(
            "In write_fe_param with params {:50.50} thread id {}".format(
                params,
                threading.current_thread().ident))

        self.command_succeeded = True
        self.fe_param_write = []

        params_by_fem = []

        for fem_idx in range(len(self.fems)):
            params_by_fem.append([])

        for idx in range(len(params)):

            param = params[idx]

            self.fe_param_write.append({})

            required_attrs = ('param', 'fem', 'chip', 'value')
            for attr in required_attrs:
                try:
                    self.fe_param_write[idx][attr] = param[attr]
                except KeyError:
                    self.command_succeeded = False
                    raise ExcaliburDetectorError(
                        'Frontend parameter write command is missing {} attribute'
                        .format(attr))
            self.fe_param_write[idx][
                'offset'] = param['offset'] if 'offset' in param else 0

            if param['param'] not in self.fe_param_map:
                self.command_succeeded = False
                raise ExcaliburDetectorError(
                    'Illegal parameter name {}'.format(param['param']))

            fem_idx_list = self._build_fem_idx_list(param['fem'])
            num_fems = len(fem_idx_list)

            values = param['value']

            # If single-valued, expand outer level of values list to match length of number of FEMs
            if len(values) == 1:
                values = [values[0]] * num_fems

            if len(values) != num_fems:
                self.command_succeeded = False
                raise ExcaliburDetectorError(
                    'Mismatch in shape of frontend parameter {} values list of FEMs'
                    .format(param['param']))

            # Remap onto per-FEM list of expanded parameters
            for (idx, fem_idx) in enumerate(fem_idx_list):

                params_by_fem[fem_idx].append({
                    'param':
                    param['param'],
                    'chip':
                    param['chip'],
                    'offset':
                    param['offset'] if 'offset' in param else 0,
                    'value':
                    values[idx]
                })

        # Execute write params for all specified FEMs (launched in threads)
        for fem_idx in fem_idx_list:
            self._increment_pending()
            self._write_fe_param(fem_idx, params_by_fem[fem_idx])

        logging.debug("write_fe_param returning")

    @run_on_executor(executor='_fem_thread_pool')
    def _write_fe_param(self, fem_idx, params):

        logging.debug("FEM {}: _write_fe_param in thread {:x}".format(
            self.fems[fem_idx].fem_id,
            threading.current_thread().ident))

        self._set_fem_error_state(fem_idx, FEM_RTN_OK, '')
        write_ok = True

        try:

            for param in params:

                param_name = param['param']
                (param_id, param_type, param_write_len, param_mode,
                 param_access) = self.fe_param_map[param_name]

                try:
                    fem_set_method = getattr(self.fems[fem_idx].fem,
                                             "set_" + param_type)

                except AttributeError:
                    self._set_fem_error_state(
                        fem_idx, FEM_RTN_INTERNALERROR,
                        'Write of frontend parameter {} failed: cannot resolve write method {}'
                        .format(param_name, 'set_' + param_type))
                    write_ok = False
                else:

                    values = param['value']

                    if param_mode == ParamPerFemRandomAccess and 'offset' in param:
                        offset = param['offset']
                    else:
                        offset = 0

                    if param_mode == ParamPerChip:
                        chip_list = param['chip']
                        if not isinstance(chip_list, list):
                            chip_list = [chip_list]
                        if chip_list == [ExcaliburDetector.ALL_CHIPS]:
                            chip_list = self.fems[fem_idx].chips_enabled

                        # If single-valued for a per-chip parameter, expand to match number of chips
                        if len(values) == 1:
                            values = [values[0]] * len(chip_list)

                        if len(values) != len(chip_list):
                            self._set_fem_error_state(fem_idx, FEM_RTN_INTERNALERROR,
                                'Write of frontend parameter {} failed: ' \
                                'mismatch between number of chips and values'.format(param_name))
                            write_ok = False
                            break

                    else:
                        chip_list = [0]

                    for (idx, chip) in enumerate(chip_list):

                        if isinstance(values[idx], list):
                            values_len = len(values[idx])
                        else:
                            values_len = 1

                        if param_mode == ParamPerFemRandomAccess:
                            # TODO validate random access offset and size don't exceed param_write_len
                            pass
                        else:
                            if values_len != param_write_len:
                                self._set_fem_error_state(fem_idx, FEM_RTN_INTERNALERROR,
                                    'Write of frontend parameter {} failed: ' \
                                    'mismatch in number of values specified (got {} expected {})'.format(
                                        param_name, values_len, param_write_len
                                    ))
                                write_ok = False
                                break

                        try:
                            rc = fem_set_method(chip, param_id, offset,
                                                values[idx])
                            if rc != FEM_RTN_OK:
                                self._set_fem_error_state(
                                    fem_idx, rc,
                                    self.fems[fem_idx].fem.get_error_msg())
                                write_ok = False

                                if rc in [
                                        FEM_RTN_CONNECTION_CLOSED,
                                        FEM_RTN_TIMEOUT
                                ]:
                                    self._do_disconnect(fem_idx)

                                break

                        except ExcaliburFemError as e:
                            self._set_fem_error_state(fem_idx,
                                                      FEM_RTN_INTERNALERROR,
                                                      str(e))
                            write_ok = False
                            break

                if not write_ok:
                    break

        except Exception as e:
            self._set_fem_error_state(
                fem_idx, FEM_RTN_INTERNALERROR,
                'Write of frontend parameter {} failed: {}'.format(param, e))
            write_ok = False

        if not write_ok:
            logging.error(self.fems[fem_idx].error_msg)

        self._decrement_pending(write_ok)
Пример #7
0
class EigerDetector(object):
    STR_API = 'api'
    STR_DETECTOR = 'detector'
    STR_MONITOR = 'monitor'
    STR_STREAM = 'stream'
    STR_FW = 'filewriter'
    STR_CONFIG = 'config'
    STR_STATUS = 'status'
    STR_COMMAND = 'command'
    STR_BOARD_000 = 'board_000'
    STR_BUILDER = 'builder'

    DETECTOR_CONFIG = [
        'auto_summation', 'beam_center_x', 'beam_center_y', 'bit_depth_image',
        'bit_depth_readout', 'chi_increment', 'chi_start', 'compression',
        'count_time', 'counting_mode', 'countrate_correction_applied',
        'countrate_correction_count_cutoff', 'data_collection_date',
        'description', 'detector_distance', 'detector_number',
        'detector_readout_time', 'element', 'flatfield',
        'flatfield_correction_applied', 'frame_time', 'kappa_increment',
        'kappa_start', 'nimages', 'ntrigger', 'number_of_excluded_pixels',
        'omega_increment', 'omega_start', 'phi_increment', 'phi_start',
        'photon_energy', 'pixel_mask', 'pixel_mask_applied', 'roi_mode',
        'sensor_material', 'sensor_thickness', 'software_version',
        'threshold_energy', 'trigger_mode', 'trigger_start_delay',
        'two_theta_increment', 'two_theta_start', 'wavelength', 'x_pixel_size',
        'x_pixels_in_detector', 'y_pixel_size', 'y_pixels_in_detector'
    ]
    DETECTOR_STATUS = [
        'state', 'error', 'time', 'link_0', 'link_1', 'link_2', 'link_3',
        'stale_parameters'
    ]
    DETECTOR_BOARD_STATUS = ['th0_temp', 'th0_humidity']
    DETECTOR_BUILD_STATUS = ['dcu_buffer_free']
    MONITOR_CONFIG = ['mode']
    STREAM_CONFIG = [
        'mode', 'header_detail', 'header_appendix', 'image_appendix'
    ]
    STREAM_STATUS = ['dropped']
    FW_CONFIG = ['mode', 'compression_enabled']

    TIFF_ID_IMAGEWIDTH = 256
    TIFF_ID_IMAGEHEIGHT = 257
    TIFF_ID_BITDEPTH = 258
    TIFF_ID_STRIPOFFSETS = 273
    TIFF_ID_ROWSPERSTRIP = 278

    # Parameters which trigger a (slow) re-calculation of the bit depth
    DETECTOR_BITDEPTH_TRIGGERS = ['photon_energy', 'threshold_energy'
                                  ]  # unused, for information
    DETECTOR_BITDEPTH_PARAM = 'bit_depth_image'

    def __init__(self, endpoint, api_version):
        # Record the connection endpoint
        self._endpoint = endpoint
        self._api_version = api_version
        self._executing = True
        self._connected = False
        self._sequence_id = 0
        self._initializing = False
        self._error = ''
        self._acquisition_complete = True
        self._armed = False
        self._live_view_enabled = False
        self._live_view_frame_number = 0

        # Re-fetch of the parameters; last fetch of certain parameters stale
        self._stale_parameters = []

        self.trigger_exposure = 0.0
        self.manual_trigger = False

        self._trigger_event = threading.Event()
        self._acquisition_event = threading.Event()
        self._initialize_event = threading.Event()

        self._detector_config_uri = '{}/{}/{}/{}'.format(
            self.STR_DETECTOR, self.STR_API, api_version, self.STR_CONFIG)
        self._detector_status_uri = '{}/{}/{}/{}'.format(
            self.STR_DETECTOR, self.STR_API, api_version, self.STR_STATUS)
        self._detector_monitor_uri = '{}/{}/{}/images/next'.format(
            self.STR_MONITOR, self.STR_API, api_version)
        self._detector_command_uri = '{}/{}/{}/{}'.format(
            self.STR_DETECTOR, self.STR_API, api_version, self.STR_COMMAND)
        self._stream_config_uri = '{}/{}/{}/{}'.format(self.STR_STREAM,
                                                       self.STR_API,
                                                       api_version,
                                                       self.STR_CONFIG)
        self._stream_status_uri = '{}/{}/{}/{}'.format(self.STR_STREAM,
                                                       self.STR_API,
                                                       api_version,
                                                       self.STR_STATUS)
        self._monitor_config_uri = '{}/{}/{}/{}'.format(
            self.STR_MONITOR, self.STR_API, api_version, self.STR_CONFIG)
        self._filewriter_config_uri = '{}/{}/{}/{}'.format(
            self.STR_FW, self.STR_API, api_version, self.STR_CONFIG)

        self.missing_parameters = []

        # Check if we need to initialize
        param = self.read_detector_status('state')
        if 'value' in param:
            if param['value'] == 'na':
                # We should re-init the detector immediately
                logging.warning(
                    "Detector found in uninitialized state at startup, initializing..."
                )
                self.write_detector_command('initialize')

        # Initialise the parameter tree structure
        param_tree = {
            self.STR_API: (lambda: 0.1, {}),
            self.STR_DETECTOR: {
                self.STR_API: {
                    self._api_version: {
                        self.STR_CONFIG: {},
                        self.STR_STATUS: {
                            self.STR_BOARD_000: {},
                            self.STR_BUILDER: {}
                        }
                    }
                }
            },
            self.STR_STREAM: {
                self.STR_API: {
                    self._api_version: {
                        self.STR_CONFIG: {},
                        self.STR_STATUS: {},
                    },
                },
            },
            self.STR_MONITOR: {
                self.STR_API: {
                    self._api_version: {
                        self.STR_CONFIG: {},
                    },
                },
            },
            self.STR_FW: {
                self.STR_API: {
                    self._api_version: {
                        self.STR_CONFIG: {},
                    },
                },
            }
        }

        # Initialise configuration parameters and populate the parameter tree
        for cfg in self.DETECTOR_CONFIG:
            param = self.read_detector_config(cfg)
            if param is not None:
                setattr(self, cfg, param)
                # Check if the config item is read/write
                writeable = False
                if 'access_mode' in param:
                    if param['access_mode'] == 'rw':
                        writeable = True

                if writeable is True:
                    param_tree[self.STR_DETECTOR][self.STR_API][
                        self._api_version][self.STR_CONFIG][cfg] = (
                            lambda x=cfg: self.get_value(getattr(self, x)),
                            lambda value, x=cfg: self.set_value(x, value),
                            self.get_meta(getattr(self, cfg)))
                else:
                    param_tree[self.STR_DETECTOR][self.STR_API][
                        self._api_version][self.STR_CONFIG][cfg] = (
                            lambda x=cfg: self.get_value(getattr(self, x)),
                            self.get_meta(getattr(self, cfg)))
            else:
                logging.error(
                    "Parameter {} has not been implemented for API {}".format(
                        cfg, self._api_version))
                self.missing_parameters.append(cfg)

        # Initialise status parameters and populate the parameter tree
        for status in self.DETECTOR_STATUS:
            try:
                reply = self.read_detector_status(status)
                if reply is not None:
                    # Test for special cases link_x.  These are enums but do not have the allowed values set in the hardware
                    if 'link_' in status:
                        reply['allowed_values'] = ['down', 'up']
                    setattr(self, status, reply)
                    if status == 'stale_parameters':
                        param_tree[self.STR_DETECTOR][self.STR_API][
                            self._api_version][self.STR_STATUS][status] = (
                                lambda x=status: self.get_value(
                                    getattr(self, x)), lambda value, x=status:
                                self.set_value(x, value),
                                self.get_meta(getattr(self, status)))
                    else:
                        param_tree[self.STR_DETECTOR][self.STR_API][
                            self._api_version][self.STR_STATUS][status] = (
                                lambda x=getattr(self, status): self.get_value(
                                    x), self.get_meta(getattr(self, status)))
                else:
                    logging.error(
                        "Status {} has not been implemented for API {}".format(
                            status, self._api_version))
                    self.missing_parameters.append(status)
            except:
                # For a 500K link_2 and link_3 status will fail and return exceptions here, which is OK
                if status == 'link_2' or status == 'link_3':
                    param_tree[self.STR_DETECTOR][self.STR_API][
                        self._api_version][self.STR_STATUS][status] = (
                            lambda: 'down', {
                                'allowed_values': ['down', 'up']
                            })
                else:
                    raise

        for status in self.DETECTOR_BOARD_STATUS:
            reply = self.read_detector_status('{}/{}'.format(
                self.STR_BOARD_000, status))
            if reply is not None:
                setattr(self, status, reply)
                param_tree[self.STR_DETECTOR][self.STR_API][self._api_version][
                    self.STR_STATUS][self.STR_BOARD_000][status] = (
                        lambda x=getattr(self, status): self.get_value(x),
                        self.get_meta(getattr(self, status)))
            else:
                logging.error(
                    "Status {} has not been implemented for API {}".format(
                        status, self._api_version))
                self.missing_parameters.append(status)

        for status in self.DETECTOR_BUILD_STATUS:
            reply = self.read_detector_status('{}/{}'.format(
                self.STR_BUILDER, status))
            if reply is not None:
                setattr(self, status, reply)
                param_tree[self.STR_DETECTOR][self.STR_API][self._api_version][
                    self.STR_STATUS][self.STR_BUILDER][status] = (
                        lambda x=getattr(self, status): self.get_value(x),
                        self.get_meta(getattr(self, status)))
            else:
                logging.error(
                    "Status {} has not been implemented for API {}".format(
                        status, self._api_version))
                self.missing_parameters.append(status)

        for status in self.STREAM_STATUS:
            reply = self.read_stream_status(status)
            if reply is not None:
                setattr(self, status, reply)
                param_tree[self.STR_STREAM][self.STR_API][self._api_version][
                    self.STR_STATUS][status] = (
                        lambda x=getattr(self, status): self.get_value(x),
                        self.get_meta(getattr(self, status)))
            else:
                logging.error(
                    "Status {} has not been implemented for API {}".format(
                        status, self._api_version))
                self.missing_parameters.append(status)

        # Initialise stream config items
        for cfg in self.STREAM_CONFIG:
            if cfg == 'mode':
                setattr(self, 'stream_mode', self.read_stream_config('mode'))
                param_tree[self.STR_STREAM][self.STR_API][self._api_version][
                    self.STR_CONFIG]['mode'] = (
                        lambda x='stream_mode': self.get_value(getattr(
                            self, x)),
                        lambda value: self.set_mode(self.STR_STREAM, value),
                        self.get_meta(getattr(self, 'stream_mode')))

            else:
                setattr(self, cfg, self.read_stream_config(cfg))
                param_tree[self.STR_STREAM][self.STR_API][self._api_version][
                    self.STR_CONFIG][cfg] = (
                        lambda x=cfg: self.get_value(getattr(self, x)),
                        lambda value, x=cfg: self.set_value(x, value),
                        self.get_meta(getattr(self, cfg)))


#param_tree[self.STR_DETECTOR][self.STR_API][self._api_version][self.STR_STATUS][status] = (lambda x=getattr(self, status): self.get_value(x), self.get_meta(getattr(self, status)))

# Initialise monitor mode
        setattr(self, 'monitor_mode', self.read_monitor_config('mode'))
        param_tree[self.STR_MONITOR][self.STR_API][self._api_version][
            self.STR_CONFIG]['mode'] = (
                lambda x='monitor_mode': self.get_value(getattr(self, x)),
                lambda value: self.set_mode(self.STR_MONITOR, value),
                self.get_meta(getattr(self, 'monitor_mode')))

        # Initialise filewriter config items
        for cfg in self.FW_CONFIG:
            if cfg == 'mode':
                # Initialise filewriter mode
                setattr(self, 'fw_mode', self.read_filewriter_config('mode'))
                param_tree[self.STR_FW][self.STR_API][self._api_version][
                    self.STR_CONFIG]['mode'] = (
                        lambda x='fw_mode': self.get_value(getattr(self, x)),
                        lambda value: self.set_mode(self.STR_FW, value),
                        self.get_meta(getattr(self, 'fw_mode')))
            else:
                setattr(self, cfg, self.read_filewriter_config(cfg))
                param_tree[self.STR_FW][self.STR_API][self._api_version][
                    self.STR_CONFIG][cfg] = (
                        lambda x=cfg: self.get_value(getattr(self, x)),
                        lambda value, x=cfg: self.set_value(x, value),
                        self.get_meta(getattr(self, cfg)))

        # Initialise additional ADOdin configuration items
        if self._api_version != '1.8.0':
            param_tree[self.STR_DETECTOR][self.STR_API][self._api_version][
                self.STR_CONFIG]['ccc_cutoff'] = (
                    lambda: self.get_value(
                        getattr(self, 'countrate_correction_count_cutoff')),
                    self.get_meta(
                        getattr(self, 'countrate_correction_count_cutoff')))
        param_tree['status'] = {
            'manufacturer': (lambda: 'Dectris', {}),
            'model': (lambda: 'Odin [Eiger {}]'.format(self._api_version), {}),
            'state': (self.get_state, {}),
            'sensor': {
                'width':
                (lambda: self.get_value(getattr(self, 'x_pixels_in_detector')),
                 self.get_meta(getattr(self, 'x_pixels_in_detector'))),
                'height':
                (lambda: self.get_value(getattr(self, 'y_pixels_in_detector')),
                 self.get_meta(getattr(self, 'y_pixels_in_detector'))),
                'bytes':
                (lambda: self.get_value(getattr(self, 'x_pixels_in_detector'))
                 * self.get_value(getattr(self, 'y_pixels_in_detector')) * self
                 .get_value(getattr(self, 'bit_depth_image')) / 8, {})
            },
            'sequence_id': (lambda: self._sequence_id, {}),
            'error': (lambda: self._error, {}),
            'acquisition_complete': (lambda: self._acquisition_complete, {}),
            'armed': (lambda: self._armed, {})
        }
        param_tree['config'] = {
            'trigger_exposure':
            (lambda: self.trigger_exposure,
             lambda value: setattr(self, 'trigger_exposure', value), {}),
            'manual_trigger':
            (lambda: self.manual_trigger,
             lambda value: setattr(self, 'manual_trigger', value), {}),
            'num_images': (lambda: self.get_value(getattr(self, 'nimages')),
                           lambda value: self.set_value('nimages', value),
                           self.get_meta(getattr(self, 'nimages'))),
            'exposure_time':
            (lambda: self.get_value(getattr(self, 'count_time')),
             lambda value: self.set_value('count_time', value),
             self.get_meta(getattr(self, 'count_time'))),
            'live_view':
            (lambda: self._live_view_enabled,
             lambda value: setattr(self, '_live_view_enabled', value), {})
        }
        param_tree[self.STR_DETECTOR][self.STR_API][self._api_version][
            self.STR_COMMAND] = {
                'initialize':
                (lambda: 0,
                 lambda value: self.write_detector_command('initialize')),
                'arm': (lambda: 0,
                        lambda value: self.write_detector_command('arm')),
                'trigger':
                (lambda: 0,
                 lambda value: self.write_detector_command('trigger')),
                'disarm':
                (lambda: 0,
                 lambda value: self.write_detector_command('disarm')),
                'cancel':
                (lambda: 0,
                 lambda value: self.write_detector_command('cancel')),
                'abort': (lambda: 0,
                          lambda value: self.write_detector_command('abort')),
                'wait': (lambda: 0,
                         lambda value: self.write_detector_command('wait'))
            }

        self._params = ParameterTree(param_tree)

        self._lv_context = zmq.Context()
        self._lv_publisher = self._lv_context.socket(zmq.PUB)
        self._lv_publisher.bind("tcp://*:5555")

        # Run the live view update thread
        self._lv_thread = threading.Thread(target=self.lv_loop)
        self._lv_thread.start()

        # Run the acquisition thread
        self._acq_thread = threading.Thread(target=self.do_acquisition)
        self._acq_thread.start()

        # Run the initialize thread
        self._init_thread = threading.Thread(target=self.do_initialize)
        self._init_thread.start()

        # Run the initialize thread
        self._status_thread = threading.Thread(target=self.do_check_status)
        self._status_thread.start()

    def read_all_config(self):
        for cfg in self.DETECTOR_CONFIG:
            param = self.read_detector_config(cfg)
            setattr(self, cfg, param)

    def get_state(self):
        odin_states = {'idle': 0, 'acquire': 1}
        # Get the detector state and map to an ADOdin state
        if 'value' in self.state:
            if self.state['value'] in odin_states:
                return odin_states[self.state['value']]
        return 0

    def get(self, path):
        # Check for ODIN specific commands
        if path == 'command/start_acquisition':
            return {'value': 0}
        elif path == 'command/stop_acquisition':
            return {'value': 0}
        elif path == 'command/send_trigger':
            return {'send_trigger': {'value': 0}}
        elif path == 'command/initialize':
            return {'initialize': {'value': self._initializing}}
        else:
            return self._params.get(path, with_metadata=True)

    def set(self, path, value):
        # Check for ODIN specific commands
        if path == 'command/start_acquisition':
            return self.start_acquisition()
        elif path == 'command/stop_acquisition':
            return self.stop_acquisition()
        elif path == 'command/send_trigger':
            return self.send_trigger()
        elif path == 'command/initialize':
            return self.initialize_detector()
        else:
            # mbbi record will send integers; change to string
            if any(option == path.split("/")[-1]
                   for option in option_config_items):
                value = str(value)
            return self._params.set(path, value)

    def get_value(self, item):
        # Check if the item has a value field. If it does then return it
        if 'value' in item:
            return item['value']
        return None

    def set_mode(self, mode_type, value):
        logging.info("Setting {} mode to {}".format(mode_type, value))
        # First write the value to the hardware
        if mode_type == self.STR_STREAM:
            response = self.write_stream_config('mode', value)
            param = self.read_stream_config('mode')
            setattr(self, 'stream_mode', param)
        elif mode_type == self.STR_MONITOR:
            response = self.write_monitor_config('mode', value)
            param = self.read_monitor_config('mode')
            setattr(self, 'monitor_mode', param)
        elif mode_type == self.STR_FW:
            response = self.write_filewriter_config('mode', value)
            param = self.read_filewriter_config('mode')
            setattr(self, 'fw_mode', param)

    def set_value(self, item, value):
        response = None
        logging.info("Setting {} to {}".format(item, value))
        # Intercept integer values and convert to string values where
        # option not index is expected
        if any(option == item for option in option_config_items):
            value = option_config_options[item].get_option(value)
        # First write the value to the hardware
        if item in self.DETECTOR_CONFIG:
            response = self.write_detector_config(item, value)
        elif item in self.STREAM_CONFIG:
            response = self.write_stream_config(item, value)
        elif item in self.FW_CONFIG:
            response = self.write_filewriter_config(item, value)

        # Now check the response to see if we need to update any config items
        if response is not None:
            if isinstance(response, list):
                self._stale_parameters = response
                self.update_stale_parameters()
        else:
            if item in self.DETECTOR_CONFIG:
                param = self.read_detector_config(item)
            elif item in self.STREAM_CONFIG:
                param = self.read_stream_config(item)
            elif item in self.FW_CONFIG:
                param = self.read_filewriter_config(item)
            elif item in self.DETECTOR_STATUS:
                param = self.read_detector_status(item)
            logging.info("Read from detector [{}]: {}".format(item, param))
            setattr(self, item, param)

    def parse_response(self, response, item):
        reply = None
        try:
            reply = json.loads(response.text)
        except:
            # If parameter unavailable, do not repeat logging
            for missing in self.missing_parameters:
                if missing in item:
                    return None
            # Unable to parse the json response, so simply log this
            logging.error("Failed to parse a JSON response: {}".format(
                response.text))
        return reply

    def get_meta(self, item):
        # Populate any meta data items and return the dict
        meta = {}
        if 'min' in item:
            meta['min'] = item['min']
        if 'max' in item:
            meta['max'] = item['max']
        if 'allowed_values' in item:
            meta['allowed_values'] = item['allowed_values']
        if 'unit' in item:
            meta['units'] = item['unit']
        return meta

    def read_detector_config(self, item):
        # Read a specifc detector config item from the hardware
        r = requests.get('http://{}/{}/{}'.format(self._endpoint,
                                                  self._detector_config_uri,
                                                  item))
        parsed_reply = self.parse_response(r, item)
        # Intercept detector config for options where we convert to index for
        # unamabiguous definition and update config to allow these
        if any(option in item for option in option_config_items):
            # Inconsitency over mapping of index to string
            # communication via integer, uniquely converted to mapping as defined in eiger_options
            value = parsed_reply[u'value']
            parsed_reply[u'value'] = option_config_options[item].get_index(
                value)
            parsed_reply[u'allowed_values'] = option_config_options[
                item].get_allowed_values()
        return parsed_reply

    def write_detector_config(self, item, value):
        # Read a specifc detector config item from the hardware
        r = requests.put('http://{}/{}/{}'.format(self._endpoint,
                                                  self._detector_config_uri,
                                                  item),
                         data=json.dumps({'value': value}),
                         headers={"Content-Type": "application/json"})
        return self.parse_response(r, item)

    def read_detector_status(self, item):
        if item == 'stale_parameters':
            # Read stale parameters flag
            response = {
                u'value': len(self._stale_parameters) != 0,
                u'access_mode': u'r',
                u'value_type': u'bool'
            }
            return response
        else:
            # Read a specifc detector status item from the hardware
            r = requests.get('http://{}/{}/{}'.format(
                self._endpoint, self._detector_status_uri, item))
            return self.parse_response(r, item)

    def write_detector_command(self, command, value=None):
        # Write a detector specific command to the detector
        reply = None
        data = None
        if value is not None:
            data = json.dumps({'value': value})
        r = requests.put('http://{}/{}/{}'.format(self._endpoint,
                                                  self._detector_command_uri,
                                                  command),
                         data=data,
                         headers={"Content-Type": "application/json"})
        if len(r.text) > 0:
            reply = self.parse_response(r, command)
        return reply

    def read_stream_config(self, item):
        # Read a specifc detector config item from the hardware
        r = requests.get('http://{}/{}/{}'.format(self._endpoint,
                                                  self._stream_config_uri,
                                                  item))
        parsed_reply = self.parse_response(r, item)
        if any(option in item for option in option_config_items):
            # Inconsitency over mapping of index to string
            # communication via integer, uniquely converted to mapping as defined in eiger_options
            value = parsed_reply[u'value']
            parsed_reply[u'value'] = option_config_options[item].get_index(
                value)
            parsed_reply[u'allowed_values'] = option_config_options[
                item].get_allowed_values()
        return parsed_reply

    def write_stream_config(self, item, value):
        # Read a specifc detector config item from the hardware
        r = requests.put('http://{}/{}/{}'.format(self._endpoint,
                                                  self._stream_config_uri,
                                                  item),
                         data=json.dumps({'value': value}),
                         headers={"Content-Type": "application/json"})
        return self.parse_response(r, item)

    def read_stream_status(self, item):
        # Read a specifc stream status item from the hardware
        r = requests.get('http://{}/{}/{}'.format(self._endpoint,
                                                  self._stream_status_uri,
                                                  item))
        return self.parse_response(r, item)

    def read_monitor_config(self, item):
        # Read a specifc monitor config item from the hardware
        r = requests.get('http://{}/{}/{}'.format(self._endpoint,
                                                  self._monitor_config_uri,
                                                  item))
        return self.parse_response(r, item)

    def write_monitor_config(self, item, value):
        # Read a specifc detector config item from the hardware
        r = requests.put('http://{}/{}/{}'.format(self._endpoint,
                                                  self._monitor_config_uri,
                                                  item),
                         data=json.dumps({'value': value}),
                         headers={"Content-Type": "application/json"})
        return self.parse_response(r, item)

    def read_filewriter_config(self, item):
        # Read a specifc filewriter config item from the hardware
        r = requests.get('http://{}/{}/{}'.format(self._endpoint,
                                                  self._filewriter_config_uri,
                                                  item))
        return self.parse_response(r, item)

    def write_filewriter_config(self, item, value):
        # Write a specifc filewriter config item to the hardware
        r = requests.put('http://{}/{}/{}'.format(self._endpoint,
                                                  self._filewriter_config_uri,
                                                  item),
                         data=json.dumps({'value': value}),
                         headers={"Content-Type": "application/json"})
        return self.parse_response(r, item)

    def read_detector_live_image(self):
        # Read the relevant monitor stream
        r = requests.get('http://{}/{}'.format(self._endpoint,
                                               self._detector_monitor_uri))
        if r.content == 'Image not available' or r.content == "no data available":
            # There is no live image (1.6.0 or 1.8.0) so we can just pass through
            return
        else:
            tiff = r.content
            # Read the header information from the image
            logging.debug("Size of raw stream input: {}".format(len(tiff)))
            hdr = tiff[4:8]
            index_offset = struct.unpack("=i", hdr)[0]
            hdr = tiff[index_offset:index_offset + 2]
            logging.debug("Number of tags: {}".format(
                struct.unpack("=h", hdr)[0]))
            number_of_tags = struct.unpack("=h", hdr)[0]

            image_width = -1
            image_height = -1
            image_bitdepth = -1
            image_rows_per_strip = -1
            image_strip_offset = -1

            for tag_index in range(number_of_tags):
                tag_offset = index_offset + 2 + (tag_index * 12)
                logging.debug("Tag number {}: entry offset: {}".format(
                    tag_index, tag_offset))
                hdr = tiff[tag_offset:tag_offset + 2]
                tag_id = struct.unpack("=h", hdr)[0]
                hdr = tiff[tag_offset + 2:tag_offset + 4]
                tag_data_type = struct.unpack("=h", hdr)[0]
                hdr = tiff[tag_offset + 4:tag_offset + 8]
                tag_data_count = struct.unpack("=i", hdr)[0]
                hdr = tiff[tag_offset + 8:tag_offset + 12]
                tag_data_offset = struct.unpack("=i", hdr)[0]

                logging.debug("   Tag ID: {}".format(tag_id))
                logging.debug("   Tag Data Type: {}".format(tag_data_type))
                logging.debug("   Tag Data Count: {}".format(tag_data_count))
                logging.debug("   Tag Data Offset: {}".format(tag_data_offset))

                # Now check for the width, hieght, bitdepth, rows per strip, and strip offsets
                if tag_id == self.TIFF_ID_IMAGEWIDTH:
                    image_width = tag_data_offset
                elif tag_id == self.TIFF_ID_IMAGEHEIGHT:
                    image_height = tag_data_offset
                elif tag_id == self.TIFF_ID_BITDEPTH:
                    image_bitdepth = tag_data_offset
                elif tag_id == self.TIFF_ID_ROWSPERSTRIP:
                    image_rows_per_strip = tag_data_offset
                elif tag_id == self.TIFF_ID_STRIPOFFSETS:
                    image_strip_offset = tag_data_offset

            if image_width > -1 and image_height > -1 and image_bitdepth > -1 and image_rows_per_strip == image_height and image_strip_offset > -1:
                # We have a valid image so construct the required object and publish it
                frame_header = {
                    'frame_num': self._live_view_frame_number,
                    'acquisition_id': '',
                    'dtype': 'uint{}'.format(image_bitdepth),
                    'dsize': image_bitdepth / 8,
                    'dataset': 'data',
                    'compression': 0,
                    'shape':
                    ["{}".format(image_height), "{}".format(image_width)]
                }
                logging.info("Frame object created: {}".format(frame_header))

                frame_data = tiff[image_strip_offset:image_strip_offset +
                                  (image_width * image_height *
                                   image_bitdepth / 8)]

                self._lv_publisher.send_json(frame_header, flags=zmq.SNDMORE)
                self._lv_publisher.send(frame_data, 0)
                self._live_view_frame_number += 1

    def arm_detector(self):
        # Write a detector specific command to the detector
        logging.info("Arming the detector")
        s_obj = self.write_detector_command('arm')
        # We are looking for the sequence ID
        self._sequence_id = s_obj['sequence id']
        logging.info("Arm complete, returned sequence ID: {}".format(
            self._sequence_id))

    def initialize_detector(self):
        self._initializing = True
        # Write a detector specific command to the detector
        logging.info("Initializing the detector")
        self._initialize_event.set()

    def fetch_stale_parameters(self):
        for cfg in self._stale_parameters:
            if cfg in self.DETECTOR_CONFIG:
                param = self.read_detector_config(cfg)
            elif cfg in self.STREAM_CONFIG:
                param = self.read_stream_config(cfg)
            logging.info("Read from detector [{}]: {}".format(cfg, param))
            setattr(self, cfg, param)
        self._stale_parameters = []
        self.update_stale_parameters()

    def update_stale_parameters(self):
        setattr(self, 'stale_parameters',
                self.read_detector_status('stale_parameters'))
        if hasattr(self, '_params'):
            self.set('{}/stale_parameters'.format(self._detector_status_uri),
                     len(self._stale_parameters) != 0)

    def get_trigger_mode(self):
        trigger_idx = self.get_value(self.trigger_mode)
        return option_config_options['trigger_mode'].get_option(trigger_idx)

    def start_acquisition(self):
        # Perform the start sequence
        logging.info("Start acquisition called")

        # Fetch stale parameters
        self.fetch_stale_parameters()

        # Set the acquisition complete to false
        self._acquisition_complete = False

        # Check the trigger mode
        trigger_mode = self.get_trigger_mode()
        logging.info("trigger_mode: {}".format(trigger_mode))
        if trigger_mode == "inte" or trigger_mode == "exte":
            self.set('{}/nimages'.format(self._detector_config_uri), 1)

        # Now arm the detector
        self.arm_detector()

        # Start the acquisition thread
        if trigger_mode == "ints" or trigger_mode == "inte":
            self._acquisition_event.set()

        # Set the detector armed state to true
        self._armed = True

    def do_acquisition(self):
        while self._executing:
            if self._acquisition_event.wait(0.5):
                # Clear the acquisition event
                self._acquisition_event.clear()

                # Set the number of triggers to zero
                triggers = 0
                # Clear the trigger event
                self._trigger_event.clear()
                while self._acquisition_complete == False and triggers < self.get_value(
                        self.ntrigger):

                    do_trigger = True

                    if self.manual_trigger:
                        do_trigger = self._trigger_event.wait(0.1)

                    if do_trigger:

                        # Send the trigger to the detector
                        trigger_mode = self.get_trigger_mode()
                        logging.info(
                            "Sending trigger to the detector {}".format(
                                trigger_mode))

                        if trigger_mode == "inte":
                            self.write_detector_command(
                                'trigger', self.trigger_exposure)
                            time.sleep(self.trigger_exposure)
                        else:
                            self.write_detector_command('trigger')

                        # Increment the trigger count
                        triggers += 1
                        # Clear the trigger event
                        self._trigger_event.clear()

                self._acquisition_complete = True
                self.write_detector_command('disarm')
                self._armed = False

    def do_initialize(self):
        while self._executing:
            if self._initialize_event.wait(1.0):
                self.write_detector_command('initialize')
                # We are looking for the sequence ID
                logging.info("Initializing complete")
                self._initializing = False
                self._initialize_event.clear()
                self.read_all_config()

    def do_check_status(self):
        while self._executing:
            for status in self.DETECTOR_STATUS:
                if status not in self.missing_parameters:
                    try:
                        if status == 'link_2' or status == 'link_3':
                            if '500K' not in self.get_value(
                                    getattr(self, 'description')):
                                setattr(self, status,
                                        self.read_detector_status(status))
                        else:
                            setattr(self, status,
                                    self.read_detector_status(status))
                    except:
                        pass
            # Update bit depth if it needs updating
            if self._acquisition_complete:
                try:
                    self.fetch_stale_parameters()
                except:
                    pass
            for status in self.DETECTOR_BOARD_STATUS:
                try:
                    setattr(
                        self, status,
                        self.read_detector_status('{}/{}'.format(
                            self.STR_BOARD_000, status)))
                except:
                    pass
            for status in self.DETECTOR_BUILD_STATUS:
                try:
                    setattr(
                        self, status,
                        self.read_detector_status('{}/{}'.format(
                            self.STR_BUILDER, status)))
                except:
                    pass
            time.sleep(.5)

    def stop_acquisition(self):
        # Perform an abort sequence
        logging.info("Stop acquisition called")
        self.write_detector_command('disarm')
        self._acquisition_complete = True
        self._armed = False

    def send_trigger(self):
        # Send a manual trigger
        logging.info("Initiating a manual trigger")
        self._trigger_event.set()

    def lv_loop(self):
        while self._executing:
            if self._live_view_enabled:
                self.read_detector_live_image()
            time.sleep(0.1)

    def shutdown(self):
        self._executing = False
Пример #8
0
class Fem():
    """
    FEM object, representing a single FEM-II module.

    Facilitates communication to the underlying hardware resources 
    onbaord the FEM-II.

    GPIO    0x00    
    """
    def __init__(self):
        try:
            #BELOW: list of status register names and the corresponding GPIO port address
            self.status_register={"DONE":1006,"P1V0_MGT_PGOOD":1005,"QDR_TERM_PGOOD":1004,"DDR3_TERM_PGOOD":1003,"P1V8_MGT_PGOOD":1002,"P1V2_PGOOD":1001,"P1V5_PGOOD":1000,"P1V8_PGOOD":999,"P2V0_PGOOD":998,"P1V0_PGOOD":997,"P5V0_PGOOD":996,"P3V3_PGOOD":995, "QSFP_MODULE_PRESENT_U20":994, "QSFP_MODULE_PRESENT_U13":993}
            self.status_names = self.status_register.keys()
            
            #BELOW: list of reset register names and the corresponding GPIO port address
            self.reset_register={"ZYNC_F_RST":1010,"ZYNC_FW_RST_N":1011,"RESETL0":1012,"RESETL1":1013,"V7_INIT_B":1014,"V7_PRG_ZY":1015}
            self.reset_names = self.reset_register.keys()
            """
            from firmware
            ZYNC_F_RST           <= reset_gpio_wo(0);
            ZYNC_FW_RST_N        <= reset_gpio_wo(1); -- active HIGH!!! signal
            RESETL0              <= NOT reset_gpio_wo(2); -- active low signal
            RESETL1              <= NOT reset_gpio_wo(3); -- active low signal
            V7_INIT_B            <= NOT reset_gpio_wo(4); -- active low signal
            V7_PRG_ZY            <= reset_gpio_wo(5);
            """
            
            #BELOW: list of control register names and the corresponding GPIO port address
            self.control_register={"FSEL_1_DE": 986, "FSEL_0_DE": 987, "F_CLK_SEL": 988, "QSFP_I2C_SEL0": 989, "LPMODE0": 990, "LPMODE1": 991, "P1V0_EN_ZYNC": 992}
            self.control_names = self.control_register.keys()
            self.control_register_local = {"FSEL_1_DE": 0, "FSEL_0_DE": 0, "F_CLK_SEL": 0, "QSFP_I2C_SEL0": 0, "LPMODE0": 0, "LPMODE1": 0, "P1V0_EN_ZYNC": 1}

            """
             -- *** Control Register bis assignments for register control ***
            FSEL_1_DE <= control_reg(0);
            FSEL_0_DE <= control_reg(1);  
            F_CLK_SEL <= control_reg(2);
            QSFP_I2C_SEL0 <= control_reg(3);
            LPMODE0 <= control_reg(4);
            MODPRSL0 <= control_reg(5);
            LPMODE1 <= control_reg(6);
            MODPRSL1 <= control_reg(7);
            P1V0_EN_ZYNC <= control_reg(8);
            
            """
            self.selected_flash = 1 # device 1,2,3 or 4
        #exception error handling needs further improvement
        except ValueError:
            print('Non-numeric input detected.')

        print("I got here")
        self.gpio_root = '/sys/class/gpio/'
        self.gpiopath = lambda pin: os.path.join(gpio_root, 'gpio{0}'.format(pin))
        self.RoMODE = 'r'
        self.RWMODE = 'r+'
        self.WMODE = 'w'

        # try: #setup the gpio registers
        #     self.gpio_setup()
        # except BaseException as e:
        #     print("Failed to do something: ", e)
        # finally:
        #     print("Closing all gpio instances")
        #     gpio.cleanup()
        try:
            for key,val in self.control_register.items():
                ppath = str(self.gpio_root + 'gpio' + str(val))
                value = open(str(ppath + '/value'), self.RWMODE)
                direction = open(str(ppath + '/direction'), self.RoMODE)
                gpio._open[val] = gpio.PinState(value=value, direction=direction)
            
            for key,val in self.status_register.items():
                ppath = str(self.gpio_root + 'gpio' + str(val))
                value = open(str(ppath + '/value'), self.RoMODE)
                direction = open(str(ppath + '/direction'), self.RoMODE)
                gpio._open[val] = gpio.PinState(value=value, direction=direction)
            
            for key,val in self.reset_register.items():
                ppath = str(self.gpio_root + 'gpio' + str(val))
                value = open(str(ppath + '/value'), self.RWMODE)
                direction = open(str(ppath + '/direction'), self.RoMODE)
                gpio._open[val] = gpio.PinState(value=value, direction=direction)
            
            print(gpio._open)
            print(gpio._open[990].value)

        except (BaseException) as e:
            response = {'error': 'Something happened: {}'.format(str(e))}

            
        
        print(gpio._open)


        try: #populate the parameter tree
            self.param_tree = ParameterTree({
                "status":{
                    "DONE":(lambda: gpio.read(self.status_register.get("DONE")), None),
                    "P1V0_MGT_PGOOD":(lambda: gpio.read(self.status_register.get("P1V0_MGT_PGOOD")), None),
                    "QDR_TERM_PGOOD":(lambda: gpio.read(self.status_register.get("QDR_TERM_PGOOD")), None),
                    "DDR3_TERM_PGOOD":(lambda: gpio.read(self.status_register.get("DDR3_TERM_PGOOD")), None),
                    "P1V8_MGT_PGOOD":(lambda: gpio.read(self.status_register.get("P1V8_MGT_PGOOD")), None),
                    "P1V2_PGOOD":(lambda: gpio.read(self.status_register.get("P1V2_PGOOD")), None),
                    "P1V5_PGOOD":(lambda: gpio.read(self.status_register.get("P1V5_PGOOD")), None),
                    "P1V8_PGOOD":(lambda: gpio.read(self.status_register.get("P1V8_PGOOD")), None),
                    "P2V0_PGOOD":(lambda: gpio.read(self.status_register.get("P2V0_PGOOD")), None),
                    "P1V0_PGOOD":(lambda: gpio.read(self.status_register.get("P1V0_PGOOD")), None),
                    "P5V0_PGOOD":(lambda: gpio.read(self.status_register.get("P5V0_PGOOD")), None),
                    "P3V3_PGOOD":(lambda: gpio.read(self.status_register.get("P3V3_PGOOD")), None),
                    "QSFP_MODULE_PRESENT_U20_BOTn":(lambda: gpio.read(self.status_register.get("QSFP_MODULE_PRESENT_U20")), None),
                    "QSFP_MODULE_PRESENT_U13_TOPn":(lambda: gpio.read(self.status_register.get("QSFP_MODULE_PRESENT_U13")), None),
                    
                },
                "reset":{
                    "ZYNC_F_RST": (None, self.ZYNC_F_RST_set),
                    "ZYNC_FW_RST_N": (None, self.ZYNC_FW_RST_N_set),
                    "RESETL0": (None, self.RESETL0_set),
                    "RESETL1": (None, self.RESETL1_set),
                    "V7_INIT_B": (None, self.V7_INIT_B_set),
                    "RE-PROGRAM_FPGA": (None, self.V7_PRG_ZY_set)
                },
                "control":{
                    "FIRMWARE_SELECT":(lambda: self.selected_flash, self.set_flash, {"description":"flash 1 = default firmware, flash 2 = test firmware, flash 3 = test firmware, flash 4 = FLASH PROGRAMMING FIRMWARE"}),
                    "FLASH_CLOCK_SELECT":(lambda: self.read_control_reg("F_CLK_SEL"), self.F_CLK_SEL_set, {"description":"FPGA (DEFAULT/NORMAL) = 0, QSPI (PROGRAMMING FIRMWARE) = 1"}),
                    "QSFP_I2C_SELECT":(lambda: self.read_control_reg("QSFP_I2C_SEL0"),self.QSFP_I2C_SEL0_set, {"description":"changes which I2C interface is ACTIVE, 0 = U20 BOTT, 1 = U13 TOP"}),
                    "QSFP_LOW_POWER_MODE_U20_BOT":(lambda: self.read_control_reg("LPMODE0"), self.LPMODE0_set, {"description":"puts the bottom QSFP device into low power mode"}),
                    "QSFP_LOW_POWER_MODE_U13_TOP":(lambda: self.read_control_reg("LPMODE1"), self.LPMODE1_set, {"description":"puts the top QSFP device into low power mode"}),
                    "P1V0_EN_ZYNC":(lambda: self.read_control_reg("P1V0_EN_ZYNC"), self.P1V0_EN_ZYNC_set)
                    
                }         
            })



        except ValueError: #excepts need revision to be meaningful
            print('Non-numeric input detected.')


    def read_control_reg(self, value):
        return self.control_register_local.get(value)
    
    #parameter tree wrapper functions for control registers
    def F_CLK_SEL_set(self, value):
        self.control_register_local["F_CLK_SEL"]=value
        gpio.set(self.control_register.get("F_CLK_SEL"), value)
    def QSFP_I2C_SEL0_set(self, value):
        self.control_register_local["QSFP_I2C_SEL0"]=value
        gpio.set(self.control_register.get("QSFP_I2C_SEL0"), value)
    def LPMODE0_set(self, value):
        self.control_register_local["LPMODE0"]=value
        gpio.set(self.control_register.get("LPMODE0"), value)
    def MODPRSL0_set(self, value):
        self.control_register_local["MODPRSL0"]=value
        gpio.set(self.control_register.get("MODPRSL0"), value)
    def LPMODE1_set(self, value):
        self.control_register_local["LPMODE1"]=value
        gpio.set(self.control_register.get("LPMODE1"), value)
    def MODPRSL1_set(self, value):
        self.control_register_local["MODPRSL1"]=value
        gpio.set(self.control_register.get("MODPRSL1"), value)
    def P1V0_EN_ZYNC_set(self, value):
        self.control_register_local["P1V0_EN_ZYNC"]=value
        gpio.set(self.control_register.get("P1V0_EN_ZYNC"), value)
    def set_flash(self, value):
        
        if value == 1:
            gpio.set(self.control_register.get("FSEL_1_DE"), 0)
            self.control_register_local["FSEL_1_DE"] = 0
            gpio.set(self.control_register.get("FSEL_0_DE"), 0)
            self.control_register_local["FSEL_0_DE"] = 0
            self.selected_flash = value

        if value == 2:
            gpio.set(self.control_register.get("FSEL_1_DE"), 0)
            self.control_register_local["FSEL_1_DE"] = 0
            gpio.set(self.control_register.get("FSEL_0_DE"), 1)
            self.control_register_local["FSEL_0_DE"] = 1
            self.selected_flash = value
            
        if value == 3:
            gpio.set(self.control_register.get("FSEL_1_DE"), 1)
            self.control_register_local["FSEL_1_DE"] = 1
            gpio.set(self.control_register.get("FSEL_0_DE"), 0)
            self.control_register_local["FSEL_0_DE"] = 0
            self.selected_flash = value

        if value == 4:
            gpio.set(self.control_register.get("FSEL_1_DE"), 1)
            self.control_register_local["FSEL_1_DE"] = 1
            gpio.set(self.control_register.get("FSEL_0_DE"), 1)
            self.control_register_local["FSEL_0_DE"] = 1
            self.selected_flash = value
        else:
            print("Not a valid number, no change!")
        
        
    
    
    #parameter tree wrapper functions for gpio.set
    def ZYNC_F_RST_set(self, value):
        gpio.set(self.reset_register.get("ZYNC_F_RST"), value)
    def ZYNC_FW_RST_N_set(self, value):
        gpio.set(self.reset_register.get("ZYNC_FW_RST_N"), value)
    def RESETL0_set(self, value):
        gpio.set(self.reset_register.get("RESETL0"), value)
    def RESETL1_set(self, value):
        gpio.set(self.reset_register.get("RESETL1"), value)
    def V7_INIT_B_set(self, value):
        gpio.set(self.reset_register.get("V7_INIT_B"), value)
    def V7_PRG_ZY_set(self, value):
        gpio.set(self.reset_register.get("V7_PRG_ZY"), value)


    def gpio_setup(self):
        """This sets the GPIO registers up"""
        for key, val in  self.status_register.items():
            gpio.setup(val, "in")
        for key, val in  self.reset_register.items():
            gpio.setup(val, "out")
        for key, val in  self.control_register.items():
            gpio.setup(val, "out")
            gpio.set(val, self.control_register_local[key])

    def get(self, path, wants_metadata=False):
        """Main get method for the parameter tree"""
        return self.param_tree.get(path, wants_metadata)
    def set(self, path, data):
        """Main set method for the parameter tree"""
        return self.param_tree.set(path, data)
Пример #9
0
class FileInterface():
    """FileInterface: class that extracts and stores information about system-level parameters."""

    # Thread executor used for background tasks
    executor = futures.ThreadPoolExecutor(max_workers=1)

    def __init__(self, directories):
        """Initialise the FileInterface object.

        This constructor initialises the FileInterface object, building a parameter tree.
        """
        # Save arguments

        self.fp_config_files = []
        self.txt_files = []
        self.fr_config_files = []
        self.directories = directories

        self.odin_data_config_dir = directories["odin_data"]

        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version':
            version_info['version'],
            'tornado_version':
            tornado.version,
            'server_uptime': (self.get_server_uptime, None),
            'fr_config_files': (self.get_fr_config_files, None),
            'fp_config_files': (self.get_fp_config_files, None),
            'config_dir': (self.odin_data_config_dir, None)
        })

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the FileInterface adapter.
        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)

    def set(self, path, data):
        """Set parameters in the parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate FileInterfaceError.
        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise FileInterfaceError(e)

    def get_config_files(self):
        """Retrieve all of the txt configuration files in the absolute directory path.

        Clears the internal lists first to prevent circular appending at every "GET"
        """
        self.clear_lists()
        for file in os.listdir(os.path.expanduser(self.odin_data_config_dir)):
            if file.endswith('.json') and "hexitec" in file:
                self.txt_files.append(file)

    def get_fp_config_files(self):
        """Get the frame processor config files from the list of text files found.

        @returns: the fp config files list
        """
        self.get_config_files()
        for file in self.txt_files:
            if "fp" in file:
                self.fp_config_files.append(file)
        return self.fp_config_files

    def get_fr_config_files(self):
        """Get the frame receiver config files from the list of text files found.

        @returns: the fr config files list
        """
        self.get_config_files()
        for file in self.txt_files:
            if "fr" in file:
                self.fr_config_files.append(file)
        return self.fr_config_files

    def clear_lists(self):
        """Clear the text file, fr and fp config file lists."""
        self.fp_config_files = []
        self.txt_files = []
        self.fr_config_files = []
Пример #10
0
class LiveViewProxyAdapter(ApiAdapter):
    """
    Live View Proxy Adapter Class

    Implements the live view proxy adapter for odin control
    """
    def __init__(self, **kwargs):
        """
        Initialise the Adapter, using the provided configuration.
        Create the node classes for the subscriptions to multiple ZMQ sockets.
        Also create the publish socket to push frames onto.
        """
        logging.debug("Live View Proxy Adapter init called")
        super(LiveViewProxyAdapter, self).__init__(**kwargs)

        self.dest_endpoint = self.options.get(DEST_ENDPOINT_CONFIG_NAME,
                                              DEFAULT_DEST_ENDPOINT).strip()

        self.drop_warn_percent = float(
            self.options.get(DROP_WARN_CONFIG_NAME, DEFAULT_DROP_WARN_PERCENT))

        try:
            logging.debug("Connecting publish socket to endpoint: %s",
                          self.dest_endpoint)
            self.publish_channel = IpcTornadoChannel(
                IpcTornadoChannel.CHANNEL_TYPE_PUB, self.dest_endpoint)
            self.publish_channel.bind()
        except ZMQError as channel_err:
            # ZMQError raised here if the socket addr is already in use.
            logging.error("Connection Failed. Error given: %s",
                          channel_err.message)
        self.max_queue = self.options.get(QUEUE_LENGTH_CONFIG_NAME,
                                          DEFAULT_QUEUE_LENGTH)

        if SOURCE_ENDPOINTS_CONFIG_NAME in self.options:
            self.source_endpoints = []
            for target_str in self.options[SOURCE_ENDPOINTS_CONFIG_NAME].split(
                    ','):
                try:
                    # config provides the nodes as "node_name=socket_URI" pairs. Split those strings
                    (target, url) = target_str.split("=")
                    self.source_endpoints.append(
                        LiveViewProxyNode(target.strip(), url.strip(),
                                          self.drop_warn_percent,
                                          self.add_to_queue))
                except (ValueError, ZMQError):
                    logging.debug("Error parsing target list: %s", target_str)
        else:
            self.source_endpoints = [
                LiveViewProxyNode("node_1", DEFAULT_SOURCE_ENDPOINT,
                                  self.drop_warn_percent, self.add_to_queue)
            ]

        tree = {
            "target_endpoint": (lambda: self.dest_endpoint, None),
            'last_sent_frame': (lambda: self.last_sent_frame, None),
            'dropped_frames': (lambda: self.dropped_frame_count, None),
            'reset': (None, self.set_reset),
            "nodes": {}
        }
        for sub in self.source_endpoints:
            tree["nodes"][sub.name] = sub.param_tree

        self.param_tree = ParameterTree(tree)

        self.queue = PriorityQueue(self.max_queue)

        self.last_sent_frame = (0, 0)
        self.dropped_frame_count = 0

        self.get_frame_from_queue()

    def cleanup(self):
        """
        Ensure that, on shutdown, all ZMQ sockets are closed
        so that they do not linger and potentially cause issues in the future
        """
        self.publish_channel.close()
        for node in self.source_endpoints:
            node.cleanup()

    def get_frame_from_queue(self):
        """
        Loop to pop frames off the queue and send them to the destination ZMQ socket.
        """
        frame = None
        try:
            frame = self.queue.get_nowait()
            self.last_sent_frame = (frame.acq_id, frame.num)
            self.publish_channel.send_multipart(
                [frame.get_header(), frame.data])
        except QueueEmptyException:
            # queue is empty but thats fine, no need to report
            # or there'd be far too much output
            pass
        finally:
            IOLoop.instance().call_later(0, self.get_frame_from_queue)
        return frame  # returned for testing

    @response_types('application/json', default='application/json')
    def get(self, path, request):
        """
        HTTP Get Request Handler. Return the requested data from the parameter tree
        """
        response = self.param_tree.get(path)
        content_type = 'application/json'
        status = 200
        return ApiAdapterResponse(response, content_type, status)

    @response_types('application/json', default='application/json')
    def put(self, path, request):
        """
        HTTP Put Request Handler. Return the requested data after changes were made.
        """
        try:
            data = json_decode(request.body)
            self.param_tree.set(path, data)
            response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as set_err:
            response = {'error': str(set_err)}
            status_code = 400
        return ApiAdapterResponse(response, status_code=status_code)

    def add_to_queue(self, frame, source):
        """
        Add the frame to the priority queue, so long as it's "new enough" (the frame number is
        not lower than the last sent frame)
        If the queue is full, the next frame should be removed and this frame added instead.
        """
        if (frame.acq_id, frame.num) < self.last_sent_frame:
            source.dropped_frame()
            return
        try:
            self.queue.put_nowait(frame)
        except QueueFullException:
            logging.debug("Queue Full, discarding frame")
            self.queue.get_nowait()
            self.queue.put_nowait(frame)
            self.dropped_frame_count += 1

    def set_reset(self, data):
        """
        Reset the statistics for a new aquisition, setting dropped frames and sent frame
        counters back to 0
        """
        # we ignore the "data" parameter, as it doesn't matter what was actually PUT to
        # the method to reset.
        self.last_sent_frame = (0, 0)
        self.dropped_frame_count = 0
        for node in self.source_endpoints:
            node.set_reset()
Пример #11
0
class PSCUData(object):
    """Data container for a PSCU and associated quads.

    This class implements a data container and parameter tree of a PSCU,
    its assocated quad boxes and all sensors contained therein. A PSCUData
    object, asociated with a PSCU instance, forms the primary interface between,
    and data model for, the adapter and the underlying devices.
    """

    def __init__(self, *args, **kwargs):
        """Initialise the PSCUData instance.

        This constructor initialises the PSCUData instance. If an existing PSCU instance
        is passed in as a keyword argument, that is used for accessing data, otherwise
        a new instance is created.

        :param args: positional arguments to be passed if creating a new PSCU
        :param kwargs: keyword arguments to be passed if creating a new PSCU, or if
        a pscu key is present, that is used as an existing PSCU object instance
        """
        # If a PSCU has been passed in keyword arguments use that, otherwise create a new one
        if 'pscu' in kwargs:
            self.pscu = kwargs['pscu']
        else:
            self.pscu = PSCU(*args, **kwargs)

        # Get the QuadData containers associated with the PSCU
        self.quad_data = [QuadData(quad=q) for q in self.pscu.quad]

        # Get the temperature and humidity containers associated with the PSCU
        self.temperature_data = [
            TempData(self.pscu, i) for i in range(self.pscu.num_temperatures)
        ]
        self.humidity_data = [
            HumidityData(self.pscu, i) for i in range(self.pscu.num_humidities)
        ]

        # Build the parameter tree of the PSCU
        self.param_tree = ParameterTree({
            "quad": {
                "quads": [q.param_tree for q in self.quad_data],
                'trace': (self.get_quad_traces, None),
            },
            "temperature": {
                "sensors": [t.param_tree for t in self.temperature_data],
                "overall": (self.pscu.get_temperature_state,  None),
                "latched": (self.pscu.get_temperature_latched,  None),
            },
            "humidity": {
                "sensors": [h.param_tree for h in self.humidity_data],
                "overall": (self.pscu.get_humidity_state, None),
                "latched": (self.pscu.get_humidity_latched, None),
            },
            "fan": {
                "target": (self.pscu.get_fan_target, self.pscu.set_fan_target),
                "currentspeed_volts": (self.pscu.get_fan_speed_volts, None),
                "currentspeed": (self.pscu.get_fan_speed, None),
                "setpoint": (self.pscu.get_fan_set_point, None),
                "setpoint_volts": (self.pscu.get_fan_set_point_volts, None),
                "tripped": (self.pscu.get_fan_tripped, None),
                "overall": (self.pscu.get_fan_state, None),
                "latched": (self.pscu.get_fan_latched, None),
                "mode": (self.pscu.get_fan_mode, None),
            },
            "pump": {
                "flow": (self.pscu.get_pump_flow, None),
                "flow_volts": (self.pscu.get_pump_flow_volts, None),
                "setpoint": (self.pscu.get_pump_set_point, None),
                "setpoint_volts": (self.pscu.get_pump_set_point_volts, None),
                "tripped": (self.pscu.get_pump_tripped, None),
                "overall": (self.pscu.get_pump_state, None),
                "latched": (self.pscu.get_pump_latched, None),
                "mode": (self.pscu.get_pump_mode, None),
            },
            "trace": {
                 "overall": (self.pscu.get_trace_state, None),
                 "latched": (self.pscu.get_trace_latched,  None),
            },
            "position": (self.pscu.get_position, None),
            "position_volts": (self.pscu.get_position_volts, None),
            "overall": (self.pscu.get_health,  None),
            "latched": (self.get_all_latched, None),
            "armed": (self.pscu.get_armed, self.pscu.set_armed),
            "allEnabled": (self.pscu.get_all_enabled, self.pscu.enable_all),
            "enableInterval": (self.pscu.get_enable_interval, None),
            "displayError": (self.pscu.get_display_error, None),
        })

    def get(self, path):
        """Get parameters from the underlying parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate PSCUDataError.

        :param path: path of parameter tree to get
        :returns: parameter tree at that path as a dictionary
        """
        try:
            return self.param_tree.get(path)
        except ParameterTreeError as e:
            raise PSCUDataError(e)

    def set(self, path, data):
        """Set parameters in underlying parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate PSCUDataError.

        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise PSCUDataError(e)

    def get_all_latched(self):
        """Return the global latch status of the PSCU.

        This method returns the global latch status for the PSCU, which is the logical AND of
        PSCU latch channels

        :returns: global latch status as bool
        """
        return all(self.pscu.get_all_latched())

    def get_quad_traces(self):
        """Return the trace status for the quads in the PSCU.

        This method returns a dictionary of the quad trace status values for the PSCU.

        :returns: dictionary of the quad trace status
        """
        return {str(q): self.pscu.get_quad_trace(q) for q in range(self.pscu.num_quads)}
Пример #12
0
class Backplane():
    """ Backplane object, representing a single Backplane module.

    Facilitates communication to the underlying hardware resources
    onbaord the Backplane.
    """
    def __init__(self):
        #signal.signal(signal.SIGALRM, self.connect_handler)
        #signal.alarm(6)
        try:
            self.voltages = [0.0] * 16
            self.voltages_raw = [0] * 15
            """
            above: voltage names are =
            vddo, vdd_d18, vdd_d25, vdd_p18, vdd_a18_pll, vdd_a18adc, vdd_d18_pll, vdd_rst, vdd_a33, vdd_d33, vctrl_neg, vreset, vctrl_pos, aux_coarse, aux_fine,        aux_total
            | 0      1        2        3         4            5           6      | |   7       8         9        10       11       12    | |      13      14   |      |     15     |
            |-------------------------- U46, 0x34 -------------------------------| |------------------- U40, 0x36 ------------------------| |----- U? 0x0E -----|      | CALCULATED |
                                                                                                                                            |QEM-I backplane    |
                                                                                                                                            |this is an extra   |
                                                                                                                                            |module retro-fitted|
                                                                                                                                            |QEM-II backplane   |
                                                                                                                                            |this has been put  |
                                                                                                                                            |in and is U102     |
            """
            self.currents = [0.0] * 14
            self.currents_raw = [0.0] * 14
            """
            above: current names are =
            vddo, vdd_d18, vdd_d25, vdd_p18, vdd_a18_pll, vdd_a18adc, vdd_d18_pll, vdd_rst, vdd_a33, vdd_d33, vctrl_neg, vreset, vctrl_pos, dacextref
            | 0      1        2        3         4            5           6      | |   7       8         9        10       11       12          13   |
            |-------------------------- U45, 0x33 -------------------------------| |------------------- U39, 0x35 -----------------------------------|
            """

            self.adjust_resistor_raw = [0] * 8
            self.adjust_voltage = [0.0] * 8
            """ For the above variables, the indexes are true:
            0 = 0x51 = wiper 0 = AUXRESET   = tpl0102[0]    = calculated voltage only
            1 = 0x51 = wiper 1 = VCM        = tpl0102[1]    = calculated voltage only
            2 = 0x51 = wiper 0 = DACEXTREF  = tpl0102[2]    = calculated current only
            3 = 0x52 = wiper 1 = N/C        = tpl0102[3]
            4 = 0x52 = wiper 0 = VDD_RST    = tpl0102[4]    = calculated + measured with ADC    = self.voltages[7]
            5 = 0x52 = wiper 1 = VRESET     = tpl0102[5]    = calculated + measured with ADC    = self.voltages[11]
            6 = 0x53 = wiper 0 = VCTRL      = tpl0102[6]    = calculated + measured with ADC    = self.voltages[10](-ve) self.voltages[12](+ve)
            7 = 0x53 = wiper 1 = N/C        = tpl0102[7]
            """
            """BELOW: I2C devices instances"""
            self.tca = TCA9548(
                0x70, busnum=1
            )  #this is the multiplexer, the first device on the bus on the backplane
            self.ad5694 = self.tca.attach_device(
                5, ad5694, 0x0E, busnum=1
            )  #Digital to Analogue Converter 0x2E = fine adjustment (AUXSAMPLE_FILE), 0x2F coarse adjustment (AUXSAMPLE_COARSE)
            self.si570 = self.tca.attach_device(
                1, SI570, 0x5d, 'SI570',
                busnum=1)  #this creates a link to the clock
            self.tpl0102 = [
            ]  #this creates a list of tpl0102 devices (potentiometers)
            self.ad7998 = [
            ]  #this creates a list of ad7998 devices (Analog to Digital Converters)
            self.mcp23008 = []  #this creates a list of the GPIO devices
            self.i2c_init(
            )  # initialise i2c devices to the list variables above & initialise defaults
            """"BELOW: local variables for control & initialise defaults"""
            self.update = True  #This is used to enable or dissable to I2C access to the hardware (could be used to dissable when taking data)
            self.MONITOR_RESISTANCE = [
                2.5, 1, 1, 1, 10, 1, 10, 1, 1, 1, 10, 1, 10
            ]  #this list defines the resistance of the current-monitoring resistor in the circuit multiplied by 100 (for the amplifier)
            self.power_good = [
                False
            ] * 8  #Power goor array to indicate the status of the power suppy power-good indicators
            self.voltChannelLookup = (
                (0, 2, 3, 4, 5, 6, 7), (0, 2, 4, 5, 6, 7)
            )  #this is used to lookup the chip and channel for the voltage and current measurements
            self.si570.set_frequency(17.5)
            self.clock_frequency = 17.5  # local variable used to read pre-set clock frequency rather than reading i2c each time

        #exception error handling needs further improvement
        except ValueError:
            print('Non-numeric input detected.')

        except ImportError:
            print('Unable to locate the module.')

        try:

            #populate the parameter tree
            self.param_tree = ParameterTree({
                "VDDO": {
                    "voltage": (lambda: self.voltages[0], None, {
                        "description": "Sensor main 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[0], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_D18": {
                    "voltage": (lambda: self.voltages[1], None, {
                        "description": "Sensor Digital 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[1], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_D25": {
                    "voltage": (lambda: self.voltages[2], None, {
                        "description": "Sensor Digital 2.5V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[2], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_P18": {
                    "voltage": (lambda: self.voltages[3], None, {
                        "description":
                        "Sensor Programmable Gain Amplifier 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[3], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_A18_PLL": {
                    "voltage": (lambda: self.voltages[4], None, {
                        "description":
                        "Sensor Analogue & Phase Lock Loop 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[4], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_D18ADC": {
                    "voltage": (lambda: self.voltages[5], None, {
                        "description":
                        "Sensor Digital Analogue to Digital Converter 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[5], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_D18_PLL": {
                    "voltage": (lambda: self.voltages[6], None, {
                        "description":
                        "Sensor Digital Phase Lock Loop 1.8V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[6], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_A33": {
                    "voltage": (lambda: self.voltages[8], None, {
                        "description": "Sensor Analogue 3.3V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[8], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VDD_D33": {
                    "voltage": (lambda: self.voltages[9], None, {
                        "description": "Sensor Digital 3.3V supply",
                        "units": "V"
                    }),
                    "current": (lambda: self.currents[9], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "AUXSAMPLE_COARSE": {
                    "voltage":
                    (lambda: self.voltages[13], self.set_coarse_voltage, {
                        "description": "Sensor AUXSAMPLE COARSE VALUE input",
                        "units": "mV"
                    }),
                    "register":
                    (lambda: self.voltages_raw[13], self.set_coarse_register, {
                        "description": "Register Value"
                    })
                },
                "AUXSAMPLE_FINE": {
                    "voltage":
                    (lambda: self.voltages[14], self.set_fine_voltage, {
                        "description": "Sensor AUXSAMPLE FINE VALUE input",
                        "units": "uV"
                    }),
                    "register":
                    (lambda: self.voltages_raw[14], self.set_fine_register, {
                        "description": "Register Value"
                    })
                },
                "AUXSAMPLE": {
                    "voltage": (lambda: self.voltages[15], None, {
                        "description":
                        "Sum of coarse and fine settings"
                    })
                },
                #BELOW:need to add the set methods into the parameter tree
                "VDD_RST": {
                    "voltage":
                    (lambda: self.voltages[7], self.set_vdd_rst_voltage, {
                        "description":
                        "Sensor Reset point variable (1.8V - 3.3V) supply",
                        "units": "V"
                    }),
                    "register": (lambda: self.adjust_resistor_raw[4],
                                 self.set_vdd_rst_register_value, {
                                     "description": "Register Value"
                                 }),
                    "current": (lambda: self.currents[7], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VCTRL_NEG": {
                    "voltage": (lambda: self.voltages[10], None, {
                        "description":
                        "Sensor VCTRL variable (-2V - 0V) supply",
                        "units": "V"
                    }),
                    "register": (lambda: self.voltages_raw[10], None, {
                        "description": "Register Value"
                    }),
                    "current": (lambda: self.currents[10], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VRESET": {
                    "voltage":
                    (lambda: self.voltages[11], self.set_vreset_voltage, {
                        "description":
                        "Sensor VRESET variable (0V - 3.3V) supply",
                        "units": "V"
                    }),
                    "register": (lambda: self.adjust_resistor_raw[5],
                                 self.set_vreset_register_value, {
                                     "description": "Register Value"
                                 }),
                    "current": (lambda: self.currents[11], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VCTRL_POS": {
                    "voltage": (lambda: self.voltages[12], None, {
                        "description":
                        "Sensor VCTRL variable (0V - 3.3V) supply",
                        "units": "V"
                    }),
                    "register": (lambda: self.voltages_raw[12], None, {
                        "description": "Register Value"
                    }),
                    "current": (lambda: self.currents[12], None, {
                        "description": "Current being drawn by this supply",
                        "units": "mA"
                    })
                },
                "VCTRL": {
                    "voltage":
                    (lambda: self.adjust_voltage[6], self.set_vctrl_voltage, {
                        "description":
                        "calculated voltage of vctrl and vctrl set method"
                    }),
                    "register": (lambda: self.adjust_resistor_raw[6],
                                 self.set_vctrl_register_value, {
                                     "description": "Register Value"
                                 })
                },
                "AUXREST": {
                    "voltage":
                    (lambda: self.adjust_voltage[0], self.set_auxreset_voltage,
                     {
                         "description":
                         "Sensor AUXRESET variable (0V - 3.3V) supply",
                         "units": "V"
                     }),
                    "register": (lambda: self.adjust_resistor_raw[0],
                                 self.set_auxrest_register_value, {
                                     "description": "Register Value"
                                 })
                },
                "VCM": {
                    "voltage":
                    (lambda: self.adjust_voltage[1], self.set_vcm_voltage, {
                        "description":
                        "Sensor AUXRESET variable (0V - 3.3V) supply",
                        "units": "V"
                    }),
                    "register": (lambda: self.adjust_resistor_raw[1],
                                 self.set_vcm_register_value, {
                                     "description": "Register Value"
                                 })
                },

                #ABOVE: need to add set methods in the parameter tree
                "enable": (lambda: self.update, self.set_update, {
                    "description":
                    "Controls I2C activity on the backplane"
                }),
                "clock(MHz)":
                (lambda: self.clock_frequency, self.set_clock_frequency, {
                    "description": "Controls the main clock Reference",
                    "units": "MHz"
                }),
                "dacextref": {
                    "current":
                    (self.get_dacextref, self.set_dacextref_current, {
                        "description":
                        "Controls the DAC external current reference",
                        "units": "uA"
                    }),
                    "register":
                    (lambda: self.adjust_resistor_raw[2],
                     self.set_dacextref_register_value, {
                         "description":
                         "register that controls the external reference"
                     })
                },
                "status": {
                    "level1_PG": (lambda: self.power_good[0], None, {
                        "description":
                        "Level 1 of power supply sequence status"
                    }),
                    "level2_PG": (lambda: self.power_good[1], None, {
                        "description":
                        "Level 2 of power supply sequence status"
                    }),
                    "level3_PG": (lambda: self.power_good[2], None, {
                        "description":
                        "Level 3 of power supply sequence status"
                    }),
                    "level4_PG": (lambda: self.power_good[3], None, {
                        "description":
                        "Level 4 of power supply sequence status"
                    }),
                    "level5_PG": (lambda: self.power_good[4], None, {
                        "description":
                        "Level 5 of power supply sequence status"
                    }),
                    "level6_PG": (lambda: self.power_good[5], None, {
                        "description":
                        "Level 6 of power supply sequence status"
                    }),
                    "level7_PG": (lambda: self.power_good[6], None, {
                        "description":
                        "Level 7 of power supply sequence status"
                    }),
                    "level8_PG": (lambda: self.power_good[7], None, {
                        "description":
                        "Level 8 of power supply sequence status"
                    }),
                }
            })

        #excepts need revision to be meaningful
        except ValueError:
            print('Non-numeric input detected.')

        except ImportError:
            print('Unable to locate the module.')

    def i2c_init(self):
        """Initialises the I2C devices and some default values asociated with them"""
        #init below
        for i in range(4):
            self.tpl0102.append(
                self.tca.attach_device(0, TPL0102, 0x50 + i, busnum=1))
            self.ad7998.append(
                self.tca.attach_device(2, ad7998, 0x21 + i, busnum=1))
            self.tpl0102[i].set_non_volatile(False)

        #below: AUXSAMPLE: read the current value in the DAC registers
        self.voltages_raw[13] = self.ad5694.read_dac_value(1)
        self.voltages_raw[14] = self.ad5694.read_dac_value(4)
        #below: AUXSAMPLE : calculate the voltages based on the hardware constants set by the feeback resistors in the schematics
        self.voltages[13] = self.voltages_raw[
            13] * 0.0003734  #constant required as the multiplier for the hardware (see schematics)
        self.voltages[14] = self.voltages_raw[
            14] * 0.00002  #constant required as the multiplier for the hardware (see schematics)
        self.voltages[15] = self.voltages[13] + self.voltages[
            14] + 0.197  #constant is the voltage o/p from the op-amp when both i/p are zero

        self.mcp23008.append(
            self.tca.attach_device(3, MCP23008, 0x20, busnum=1))
        self.mcp23008.append(
            self.tca.attach_device(3, MCP23008, 0x21, busnum=1))
        for i in range(8):
            self.mcp23008[0].setup(i, MCP23008.IN)
        self.mcp23008[1].output(0, MCP23008.HIGH)
        self.mcp23008[1].setup(0, MCP23008.OUT)

        #voltage
        self.adjust_resistor_raw = [
            self.tpl0102[0].get_wiper(0, force=True),
            self.tpl0102[0].get_wiper(1, force=True),
            self.tpl0102[1].get_wiper(0, force=True),
            self.tpl0102[1].get_wiper(1, force=True),
            self.tpl0102[2].get_wiper(0, force=True),
            self.tpl0102[2].get_wiper(1, force=True),
            self.tpl0102[3].get_wiper(0, force=True),
            self.tpl0102[3].get_wiper(1, force=True)
        ]
        print(self.adjust_resistor_raw)

        #self.adjust_voltage[0] = 3.3 * (390 * self.adjust_resistor_raw[0]) / (390 * self.adjust_resistor_raw[0] + 32000)
        #self.adjust_voltage[1] = 3.3 * (390 * self.adjust_resistor_raw[1]) / (390 * self.adjust_resistor_raw[1] + 32000)
        #self.adjust_voltage[6]=-3.775 + (1.225/22600 + .35*.000001) * (390 * self.adjust_resistor_raw[6] + 32400)

        self.load_defaults()

    #Functions below are used to modify the register value on the variable supplies
    #VDD_RST & VRESET are voltages monitored by the ADC's on the module
    def set_vdd_rst_register_value(self, value):
        """Method to change the register value of VDD_RST"""
        self.tpl0102[2].set_wiper(0, value)
        self.adjust_resistor_raw[4] = self.tpl0102[2].get_wiper(0)

    def set_vdd_rst_voltage(self, value):
        """Method to change the voltage value of VDD_RST"""
        self.tpl0102[2].set_wiper(
            0,
            int(1 + (18200 / 0.0001) * (value - 1.78) /
                (390 * 18200 - 390 * (value - 1.78) / 0.0001)))
        self.adjust_resistor_raw[4] = self.tpl0102[2].get_wiper(0)

    def set_vreset_register_value(self, value):
        """Method to change the register value of VRESET"""
        self.tpl0102[2].set_wiper(1, value)
        self.adjust_resistor_raw[5] = self.tpl0102[2].get_wiper(1)

    def set_vreset_voltage(self, value):
        """Method to change the voltage value of VRESET"""
        self.tpl0102[2].set_wiper(
            1,
            int(1 + (49900 / 0.0001) * value /
                (390 * 49900 - 390 * value / 0.0001)))
        self.adjust_resistor_raw[5] = self.tpl0102[2].get_wiper(1)

    # The following voltages are calculated and NOT monitored with an ADC on the module
    def calc_vctrl_voltage(self, value):
        return -3.775 + (1.225 / 22600 + .35 * .000001) * (
            390 * self.adjust_resistor_raw[6] + 32400)

    def set_vctrl_register_value(self, value):
        self.tpl0102[3].set_wiper(0, value)
        self.adjust_resistor_raw[6] = self.tpl0102[3].get_wiper(0)
        self.adjust_voltage[6] = self.calc_vctrl_voltage(6)

    def set_vctrl_voltage(self, value):
        self.tpl0102[3].set_wiper(
            0,
            int(1 + ((value + 3.775) /
                     (1.225 / 22600 + .35 * .000001) - 32400) / 390))
        self.adjust_resistor_raw[6] = self.tpl0102[3].get_wiper(0)
        self.adjust_voltage[6] = self.calc_vctrl_voltage(6)

    # AUX & VCM use the same calculation for the voltage / register values
    def calc_aux_vcm_voltage(self, value):
        """Same calculation required for AEXRESET and VCM voltages on the backplane"""
        return 3.3 * (390 * self.adjust_resistor_raw[value]) / (
            390 * self.adjust_resistor_raw[value] + 32000)

    def calc_aux_vcm_register(self, value):
        """Same calculation required for AUXREST and VCM to calculate the register value from a voltage"""
        return int(0.5 + (32000 / 3.3) * value / (390 - 390 * value / 3.3))

    def set_aux_vcm_register_value(self, wiper, vector, value):
        """Sets the register value, pass vector number and value"""
        self.tpl0102[0].set_wiper(wiper, value)
        self.adjust_resistor_raw[vector] = self.tpl0102[0].get_wiper(wiper)
        self.adjust_voltage[vector] = self.calc_aux_vcm_voltage(vector)

    def set_aux_vcm_voltage(self, wiper, vector, value):
        """Sets the voltage for AUXSAMPLE and VCM"""
        self.tpl0102[0].set_wiper(wiper, self.calc_aux_vcm_register(value))
        self.adjust_resistor_raw[vector] = self.tpl0102[0].get_wiper(wiper)
        self.adjust_voltage[vector] = self.calc_aux_vcm_voltage(vector)

    # wrappers from the paramter tree for AUXRESET and VCM
    def set_auxrest_register_value(self, value):
        """wrapper for auxreset, pass wiper=0, vector=0, value"""
        self.set_aux_vcm_register_value(0, 0, value)

    def set_auxreset_voltage(self, value):
        """Wrapper for auxreset to set a voltage, pass wiper=0, vector=0, value"""
        self.set_aux_vcm_voltage(0, 0, value)

    def set_vcm_register_value(self, value):
        """Set VCM register value wrapper, pass wiper=1, vector = 1, value"""
        self.set_aux_vcm_register_value(1, 1, value)

    def set_vcm_voltage(self, value):
        "set VCM voltage wrapper, pass wiper, vector, value"
        self.set_aux_vcm_voltage(1, 1, value)

    # This function sets the default settings for the backplane (known working set)
    def load_defaults(self):
        """
        AUXRESET=1.9V
        VCM=1.39V
        DACEXTREF=11uA
        VDD_RST=3.3V
        VRESET=1.3V
        VCTRL=0V
        """
        self.set_vdd_rst_voltage(3.28)
        self.set_vcm_voltage(1.39)
        self.set_auxreset_voltage(1.9)
        self.set_vctrl_voltage(0)
        self.set_vreset_voltage(1.3)
        self.set_dacextref_current(11)

    #clock functions
    def get_clock_frequency(self):
        """This returns the clock frequency in MHz"""
        return self.clock_frequency

    def set_clock_frequency(self, value):
        """This sets the clock frequency in MHz"""
        self.clock_frequency = value

    def get(self, path, wants_metadata=False):
        """Main get method for the parameter tree"""
        return self.param_tree.get(path, wants_metadata)

    def set(self, path, data):
        """Main set method for the parameter tree"""
        return self.param_tree.set(path, data)

    #method to set the update flag
    def set_update(self, value):
        """This enables / disables I2C communication on the backplane"""
        self.update = value

    #functions to control the external chip current DACEXTREF - START
    def set_dacextref_register_value(self, value):
        """Method to set the register value of the DAXEXTREF, attached to list tpl0102[1] and wiper 0"""
        self.tpl0102[1].set_wiper(0, value)
        self.adjust_resistor_raw[2] = self.tpl0102[1].get_wiper(0)

    def get_dacextref(self):
        """This returns the DAC External current reference, this is not measured, just calculated
        constants are: 400 (400mV voltage reference), 390 (390 Ohms per step on programmable resistor), 294000 (R108 Resistor 294K)
        see pc3611m1 pg.6
        """
        return (400 * (390 * self.adjust_resistor_raw[2]) /
                (390 * self.adjust_resistor_raw[2] + 294000))

    def set_dacextref_current(self, value):
        """This sets the DAC external current reference with a specific current value, 294K resistor, 390 Ohm's/step, 400mV reference, see pc3611m1 pg.6"""
        self.adjust_resistor_raw[2] = int(1 + (294000 / 400) * value /
                                          (390 - 390 * value / 400))
        self.set_dacextref_register_value(self.adjust_resistor_raw[2])
        #functions to control the external chip current DACEXTREF - END

    #definitions to set coarse auxsample (1)
    def calc_coarse_common(self):
        self.voltages[13] = self.voltages_raw[13] * 0.0003734
        self.voltages[15] = self.voltages[13] + self.voltages[14] + 0.197

    def set_coarse_register(self, value):
        """This function sets the coarse register value"""
        self.voltages_raw[13] = value
        self.calc_coarse_common()
        self.ad5694.set_from_value(1, value)

    def set_coarse_voltage(self, value):
        """This function sets the coarse voltage value"""
        self.voltages_raw[13] = int(value / 0.0003734)
        self.calc_coarse_common()
        self.ad5694.set_from_voltage(1, value)

    #definitions to set fine auxsample (4)
    def calc_fine_common(self):
        self.voltages[14] = self.voltages_raw[14] * 0.00002
        self.voltages[15] = self.voltages[13] + self.voltages[14] + 0.197

    def set_fine_register(self, value):
        """This sets the fine register value"""
        self.voltages_raw[14] = value
        self.calc_fine_common()
        self.ad5694.set_from_value(4, value)

    def set_fine_voltage(self, value):
        """This sets the fine voltage value"""
        self.voltages_raw[14] = int(value / 0.00002)
        self.calc_fine_common()
        self.ad5694.set_from_voltage(4, value)

    def poll_all_sensors(self):
        """This function calls all the update functions that are executed every 1 second(s) if update = true"""
        if self.update == True:
            self.update_voltages()
            self.update_currents()
            self.power_good = self.mcp23008[0].input_pins(
                [0, 1, 2, 3, 4, 5, 6, 7, 8])

    def update_voltages(self):
        """Method to update the voltage vectors"""
        for i in range(7):
            j = self.voltChannelLookup[0][i]
            self.voltages_raw[i] = int(self.ad7998[1].read_input_raw(j)
                                       & 0xfff)
            self.voltages[i] = self.voltages_raw[i] * 3 / 4095.0
        for i in range(6):
            j = self.voltChannelLookup[1][i]
            self.voltages_raw[i + 7] = int(self.ad7998[3].read_input_raw(j)
                                           & 0xfff)
            self.voltages[i + 7] = self.voltages_raw[i + 7] * 5 / 4095.0

    def update_currents(self):
        """Method to update the current vectors"""
        for i in range(7):
            j = self.voltChannelLookup[0][i]
            self.currents_raw[i] = int(self.ad7998[0].read_input_raw(j)
                                       & 0xfff)
            self.currents[i] = self.currents_raw[i] / self.MONITOR_RESISTANCE[
                i] * (5000 / 4095.0)

        for i in range(6):
            j = self.voltChannelLookup[1][i]
            self.currents_raw[i + 7] = int(self.ad7998[2].read_input_raw(j)
                                           & 0xfff)
            self.currents[i + 7] = self.currents_raw[
                i + 7] / self.MONITOR_RESISTANCE[i + 7] * 5000 / 4095.0
Пример #13
0
class QemDetector():
    """ QemDetector object representing the entire QEM Detector System.

    Intelligent control plane that can sequence events across the subsystems lower down in the
    hierarchy to perform DAQ, calibration runs and other generic control functions on the entire
    detector system (FEM-II's, Backplane, Data Path Packages etc.)
    """
    def __init__(self, options):

        defaults = QemDetectorDefaults()
        self.file_dir = options.get("save_dir", defaults.save_dir)
        self.file_name = options.get("save_file", defaults.save_file)
        self.vector_file_dir = options.get("vector_file_dir",
                                           defaults.vector_file_dir)
        self.vector_file = options.get("vector_file_name",
                                       defaults.vector_file)
        self.acq_num = options.get("acquisition_num_frames", defaults.acq_num)
        self.acq_gap = options.get("acquisition_frame_gap", defaults.acq_gap)
        odin_data_dir = options.get("odin_data_dir", defaults.odin_data_dir)
        odin_data_dir = os.path.expanduser(odin_data_dir)

        self.daq = QemDAQ(self.file_dir,
                          self.file_name,
                          odin_data_dir=odin_data_dir)

        self.fems = []
        for key, value in options.items():
            logging.debug("%s: %s", key, value)
            if "fem" in key:
                fem_info = value.split(',')
                fem_info = [(i.split('=')[0], i.split('=')[1])
                            for i in fem_info]
                fem_dict = {
                    fem_key.strip(): fem_value.strip()
                    for (fem_key, fem_value) in fem_info
                }
                logging.debug(fem_dict)

                self.fems.append(
                    QemFem(
                        fem_dict.get("ip_addr", defaults.fem["ip_addr"]),
                        fem_dict.get("port", defaults.fem["port"]),
                        fem_dict.get("id", defaults.fem["id"]),
                        fem_dict.get("server_ctrl_ip_addr",
                                     defaults.fem["server_ctrl_ip"]),
                        fem_dict.get("camera_ctrl_ip_addr",
                                     defaults.fem["camera_ctrl_ip"]),
                        fem_dict.get("server_data_ip_addr",
                                     defaults.fem["server_data_ip"]),
                        fem_dict.get("camera_data_ip_addr",
                                     defaults.fem["camera_data_ip"]),
                        # vector file only required for the "main" FEM, fem_0
                        self.vector_file_dir,
                        self.vector_file))

        if not self.fems:  # if self.fems is empty
            self.fems.append(
                QemFem(ip_address=defaults.fem["ip_addr"],
                       port=defaults.fem["port"],
                       fem_id=defaults.fem["id"],
                       server_ctrl_ip_addr=defaults.fem["server_ctrl_ip"],
                       camera_ctrl_ip_addr=defaults.fem["camera_ctrl_ip"],
                       server_data_ip_addr=defaults.fem["server_data_ip"],
                       camera_data_ip_addr=defaults.fem["camera_data_ip"],
                       vector_file_dir=self.vector_file_dir,
                       vector_file=self.vector_file))

        fem_tree = {}
        for fem in self.fems:
            fem.connect()
            fem.setup_camera()

            fem_tree["fem_{}".format(fem.id)] = fem.param_tree

        self.file_writing = False
        self.calibrator = QemCalibrator(2000, self.fems, self.daq)
        self.param_tree = ParameterTree({
            "calibrator": self.calibrator.param_tree,
            "fems": fem_tree,
            "daq": self.daq.param_tree,
            "acquisition": {
                "num_frames": (lambda: self.acq_num, self.set_acq_num),
                "frame_gap": (lambda: self.acq_gap, self.set_acq_gap),
                "start_acq": (None, self.acquisition)
            }
        })

        self.adapters = {}

    def get(self, path):
        return self.param_tree.get(path)

    def set(self, path, data):
        # perhaps hijack the message here and run the acquisition prep
        # before passing the message on to the param_tree?
        logging.debug("SET:\n PATH: %s\n DATA: %s", path, data)
        return self.param_tree.set(path, data)

    def set_acq_num(self, num):
        logging.debug("Number Frames: %d", num)
        self.acq_num = num

    def set_acq_gap(self, gap):
        logging.debug("Frame Gap: %d", gap)
        self.acq_gap = gap

    def initialize(self, adapters):
        """Get references to required adapters and pass those references to the classes that need
            to use them
        """
        for name, adapter in adapters.items():
            if isinstance(adapter, ProxyAdapter):
                logging.debug("%s is Proxy Adapter", name)
                self.adapters["proxy"] = adapter
            elif isinstance(adapter, FrameProcessorAdapter):
                logging.debug("%s is FP Adapter", name)
                self.adapters["fp"] = adapter
            elif isinstance(adapter, FrameReceiverAdapter):
                logging.debug("%s is FR Adapter", name)
                self.adapters["fr"] = adapter
            elif isinstance(adapter, FileInterfaceAdapter):
                logging.debug("%s is File Interface Adapter", name)
                self.adapters["file_interface"] = adapter

        self.calibrator.initialize(self.adapters)
        self.daq.initialize(self.adapters)

    def cleanup(self):
        self.daq.cleanup()
        for fem in self.fems:
            fem.cleanup()

    def acquisition(self, put_data):
        if self.daq.in_progress:
            logging.warning("Cannot Start Acquistion: Already in progress")
            return
        self.daq.start_acquisition(self.acq_num)
        for fem in self.fems:
            fem.setup_camera()
            fem.get_aligner_status()  # TODO: is this required?
            locked = fem.get_idelay_lock_status()
            if not locked:
                fem.load_vectors_from_file()
        self.fems[0].frame_gate_settings(self.acq_num - 1, self.acq_gap)
        self.fems[0].frame_gate_trigger()
Пример #14
0
class SystemStatus(with_metaclass(Singleton, object)):
    """Class to monitor disks, network and processes running on a server."""
    def __init__(self, *args, **kwargs):
        """Initalise the Server Monitor.

        Creates the parameter tree for status and process monitoring.
        """
        self._log = logging.getLogger(".".join([__name__, self.__class__.__name__]))
        self._processes = {}
        self._process_status = {}
        self._interfaces = []
        self._interface_status = {}
        self._disks = []
        self._disk_status = {}

        # The parameter tree will contain general server information as well as information
        # relating to each process.  We need to initialise the top level tree
        tree = {
            'status': {
                'disk': (self.get_disk_status, None),
                'network': (self.get_interface_status, None),
                'process': (self.get_process_status, None)
            }
        }

        # Add any disks that we need to monitor
        if 'disks' in kwargs:
            disks = kwargs['disks'].split(',')
            for disk in disks:
                if os.path.isdir(disk.strip()):
                    self._disks.append(disk.strip())

        for disk in self._disks:
            self._disk_status[disk.replace("/", "_")] = {
                'total': None,
                'used': None,
                'free': None,
                'percent': None
            }

        # Add any network interfaces that we need to monitor
        available_interfaces = list(psutil.net_io_counters(pernic=True))
        if 'interfaces' in kwargs:
            interfaces = kwargs['interfaces'].split(',')
            for interface in interfaces:
                if interface.strip() in available_interfaces:
                    self._interfaces.append(interface.strip())

        for interface in self._interfaces:
            self._interface_status[interface] = {
                'bytes_sent': None,
                'bytes_recv': None,
                'packets_sent': None,
                'packets_recv': None,
                'errin': None,
                'errout': None,
                'dropin': None,
                'dropout': None
            }

        # Add any processes that we need to monitor
        if 'processes' in kwargs:
            processes = kwargs['processes'].split(',')
            for process in processes:
                self.add_processes(process.strip())

        for process in self._processes:
            self._process_status[process] = {}

        self._status = ParameterTree(tree)

        # Setup the time between status updates
        if 'rate' in kwargs:
            self._update_interval = float(1.0 / kwargs['rate'])
        else:
            self._update_interval = 1.0
        self.update_loop()

    def get_disk_status(self):
        """Return disk status information."""
        return self._disk_status

    def get_interface_status(self):
        """Return network status information."""
        return self._interface_status

    def get_process_status(self):
        """Return process status information."""
        return self._process_status

    def get(self, path):
        """Return the requested path value"""
        return self._status.get(path)

    def update_loop(self):
        """Handle update loop tasks.
        This method handles background update tasks executed periodically in the tornado
        IOLoop instance. This includes monitoring all status from the server.
        """
        try:
            self.monitor()
        except Exception as exc:
            # Nothing to do here except log the error
            self._log.exception(exc)

        # Schedule the update loop to run in the IOLoop instance again after appropriate interval
        IOLoop.instance().call_later(self._update_interval, self.update_loop)

    def add_processes(self, process_name):
        """Add a new process to monitor.

        :param process_name the name of the process to monitor
        """
        if process_name not in self._processes:
            self._log.debug("Adding process %s to monitor list", process_name)
            try:
                self._processes[process_name] = self.find_processes(process_name)
                self._log.debug(
                    "Found %d proceses with name %s",
                    len(self._processes[process_name]), process_name
                )

            except Exception as exc:
                self._log.debug(
                    "Unable to add process %s to the monitor list: %s",
                    process_name, str(exc)
                )

    def monitor(self):
        """Executed at regular interval.  Calls the specific monitoring methods."""
        self.monitor_disks()
        self.monitor_network()
        self.monitor_processes()

    def monitor_disks(self):
        """Loops over disks and retrieves the usage statistics."""
        for disk in self._disks:
            try:
                usage = psutil.disk_usage(disk)
                path = str(disk.replace("/", "_"))
                self._disk_status[path]['total'] = usage.total
                self._disk_status[path]['used'] = usage.used
                self._disk_status[path]['free'] = usage.free
                self._disk_status[path]['percent'] = usage.percent
            except Exception as exc:
                self._log.exception(exc)

    def monitor_network(self):
        """Loops over interfaces and retrieves the usage statistics."""
        try:
            network = psutil.net_io_counters(pernic=True)
            for interface in self._interfaces:
                self._interface_status[interface]['bytes_sent'] = network[interface].bytes_sent
                self._interface_status[interface]['bytes_recv'] = network[interface].bytes_recv
                self._interface_status[interface]['packets_sent'] = network[interface].packets_sent
                self._interface_status[interface]['packets_recv'] = network[interface].packets_recv
                self._interface_status[interface]['errin'] = network[interface].errin
                self._interface_status[interface]['errout'] = network[interface].errout
                self._interface_status[interface]['dropin'] = network[interface].dropin
                self._interface_status[interface]['dropout'] = network[interface].dropout
        except Exception as exc:
            self._log.exception(exc)

    def monitor_processes(self):
        """Loops over active processes and retrieves the statistics from them."""

        for process_name in self._processes:

            self._process_status[process_name] = {}

            num_processes_old = len(self._processes[process_name])
            self._processes[process_name] = self.find_processes(process_name)

            if len(self._processes[process_name]) != num_processes_old:
                self._log.debug(
                    "Number of processes named %s is now %d",
                    process_name, len(self._processes[process_name])
                )

            for process in self._processes[process_name]:
                process_status = {}
                try:
                    pid = process.pid
                    memory_info = process.memory_info()

                    process_status['cpu_percent'] = process.cpu_percent(interval=0.0)
                    if hasattr(process, 'cpu_affinity'):
                        process_status['cpu_affinity'] = process.cpu_affinity()
                    else:
                        process_status['cpu_affinity'] = None
                    process_status['memory_percent'] = process.memory_percent()

                    process_status['memory_rss'] = getattr(
                        memory_info, 'rss', None
                    )
                    process_status['memory_vms'] = getattr(
                        memory_info, 'vms', None
                    )
                    process_status['memory_shared'] = getattr(
                        memory_info, 'shared', None
                    )

                except psutil.NoSuchProcess:
                    self._log.error("Process %s no longer exists", process_name)
                except psutil.AccessDenied:
                    self._log.error("Access to process %s denied by operating system", process_name)
                else:
                    self._process_status[process_name][pid] = process_status

    def find_processes(self, process_name):
        """Find processes matching a name and return a list of process objects.

        Returns a list of psutil process object
        """
        processes = []

        parents = self.find_processes_by_name(process_name)
        for parent in parents:
            if parent.children():
                process = parent.children()[0]
            else:
                process = parent

            # Attempt to access process and remove if access denied
            try:
                _ = process.cpu_percent()
            except psutil.AccessDenied:
                pass
            else:
                processes.append(process)

        return processes

    def find_processes_by_name(self, name):
        """Return a list of process matching 'name' that is not this process (in the case
        where the process name was passed as an argument to this process)."""
        processes = []
        for proc in psutil.process_iter():
            process = None
            try:
                if name in proc.name():
                    process = proc
                else:
                    for cmdline in proc.cmdline():
                        if name in cmdline:
                            # Make sure the name isn't found as an argument to this process!
                            if os.getpid() != proc.pid:
                                process = proc
            except (psutil.AccessDenied, psutil.ZombieProcess, psutil.NoSuchProcess):
                # If we cannot access the info of this process or it is a zombie or no longer
                # exists, move on
                pass

            if process is not None:
                if process.status() not in (
                        psutil.STATUS_ZOMBIE, psutil.STATUS_STOPPED, psutil.STATUS_DEAD
                ):
                    processes.append(process)

        return processes
Пример #15
0
class SystemInfo(with_metaclass(Singleton, object)):
    """SystemInfo - class that extracts and stores information about system-level parameters."""

    # __metaclass__ = Singleton

    def __init__(self):
        """Initialise the SystemInfo object.

        This constructor initlialises the SystemInfo object, extracting various system-level
        parameters and storing them in a parameter tree to be accessible to clients.
        """
        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Extract platform information and store in parameter tree
        (system, node, release, version, _, processor) = platform.uname()
        platform_tree = ParameterTree({
            'name': 'platform',
            'description': "Information about the underlying platform",
            'system': (lambda: system, {
                "name": "system",
                "description": "operating system name",
            }),
            'node': (lambda: node, {
                "name": "node",
                "description": "node (host) name",
            }),
            'release': (lambda: release, {
                "name": "release",
                "description": "operating system release",
            }),
            'version': (lambda: version, {
                "name": "version",
                "description": "operating system version",
            }),
            'processor': (lambda: processor, {
                "name": "processor",
                "description": "processor (CPU) name",
            }),
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'name': 'system_info',
            'description': 'Information about the system hosting this odin server instance',
            'odin_version': (lambda: version_info['version'], {
                "name": "odin version",
                "description": "ODIN server version",
            }),
            'tornado_version': (lambda: tornado.version, {
                "name": "tornado version",
                "description": "version of tornado used in this server",
            }),
            'python_version': (lambda: platform.python_version(), {
                "name": "python version",
                "description": "version of python running this server",
            }),
            'platform': platform_tree,
            'server_uptime': (self.get_server_uptime, {
                "name": "server uptime",
                "description": "time since the ODIN server started",
                "units": "s",
                "display_precision": 2,
            }),
        })

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path, with_metadata=False):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the SystemInfo adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path, with_metadata)
Пример #16
0
class LiveViewer(object):
    """
    Live viewer main class.

    This class handles the major logic of the adapter, including generation of the images from data.
    """
    def __init__(self, endpoints, default_colormap):
        """
        Initialise the LiveViewer object.

        This method creates the IPC channel used to receive images from odin-data and
        assigns a callback method that is called when data arrives at the channel.
        It also initialises the Parameter tree used for HTTP GET and SET requests.
        :param endpoints: the endpoint address that the IPC channel subscribes to.
        """
        logging.debug("Initialising LiveViewer")

        self.img_data = np.arange(0, 1024, 1).reshape(32, 32)
        self.clip_min = None
        self.clip_max = None
        self.header = {}
        self.endpoints = endpoints
        self.ipc_channels = []
        for endpoint in self.endpoints:
            try:
                tmp_channel = SubSocket(self, endpoint)
                self.ipc_channels.append(tmp_channel)
                logging.debug("Subscribed to endpoint: %s",
                              tmp_channel.endpoint)
            except IpcChannelException as chan_error:
                logging.warning("Unable to subscribe to %s: %s", endpoint,
                                chan_error)

        logging.debug("Connected to %d endpoints", len(self.ipc_channels))

        if not self.ipc_channels:
            logging.warning(
                "Warning: No subscriptions made. Check the configuration file for valid endpoints"
            )

        # Define a list of available cv2 colormaps
        self.cv2_colormaps = {
            "Autumn": cv2.COLORMAP_AUTUMN,
            "Bone": cv2.COLORMAP_BONE,
            "Jet": cv2.COLORMAP_JET,
            "Winter": cv2.COLORMAP_WINTER,
            "Rainbow": cv2.COLORMAP_RAINBOW,
            "Ocean": cv2.COLORMAP_OCEAN,
            "Summer": cv2.COLORMAP_SUMMER,
            "Spring": cv2.COLORMAP_SPRING,
            "Cool": cv2.COLORMAP_COOL,
            "HSV": cv2.COLORMAP_HSV,
            "Pink": cv2.COLORMAP_PINK,
            "Hot": cv2.COLORMAP_HOT,
            "Parula": cv2.COLORMAP_PARULA
        }

        # Build a sorted list of colormap options mapping readable name to lowercase option
        self.colormap_options = OrderedDict()
        for colormap_name in sorted(self.cv2_colormaps.keys()):
            self.colormap_options[colormap_name.lower()] = colormap_name

        # Set the selected colormap to the default
        if default_colormap.lower() in self.colormap_options:
            self.selected_colormap = default_colormap.lower()
        else:
            self.selected_colormap = "jet"

        self.rendered_image = self.render_image()

        self.param_tree = ParameterTree({
            "name":
            "Live View Adapter",
            "endpoints": (self.get_channel_endpoints, None),
            "frame": (lambda: self.header, None),
            "colormap_options":
            self.colormap_options,
            "colormap_selected":
            (self.get_selected_colormap, self.set_selected_colormap),
            "data_min_max":
            (lambda: [int(self.img_data.min()),
                      int(self.img_data.max())], None),
            "frame_counts": (self.get_channel_counts, self.set_channel_counts),
            "clip_range":
            (lambda: [self.clip_min, self.clip_max], self.set_clip)
        })

    def get(self, path, _request=None):
        """
        Handle a HTTP get request.

        Checks if the request is for the image or another resource, and responds accordingly.
        :param path: the URI path to the resource requested
        :param request: Additional request parameters.
        :return: the requested resource,or an error message and code, if the request is invalid.
        """
        path_elems = re.split('[/?#]', path)
        if path_elems[0] == 'image':
            if self.img_data is not None:
                response = self.rendered_image
                content_type = 'image/png'
                status = 200
            else:
                response = {"response": "LiveViewAdapter: No Image Available"}
                content_type = 'application/json'
                status = 400
        else:

            response = self.param_tree.get(path)
            content_type = 'application/json'
            status = 200

        return response, content_type, status

    def set(self, path, data):
        """
        Handle a HTTP PUT i.e. set request.

        :param path: the URI path to the resource
        :param data: the data to PUT to the resource
        """
        self.param_tree.set(path, data)

    def create_image_from_socket(self, msg):
        """
        Create an image from data received on the socket.

        This callback function is called when data is ready on the IPC channel. It creates
        the image data array from the raw data sent by the Odin Data Plugin, reshaping
        it to a multi dimensional array matching the image dimensions.
        :param msg: a multipart message containing the image header, and raw image data.
        """
        # Message should be a list from multi part message.
        # First part will be the json header from the live view, second part is the raw image data
        header = json_decode(msg[0])

        # json_decode returns dictionary encoded in unicode. Convert to normal strings
        header = self.convert_to_string(header)
        logging.debug("Got image with header: %s", header)

        # create a np array of the image data, of type specified in the frame header
        img_data = np.fromstring(msg[1], dtype=np.dtype(header['dtype']))

        self.img_data = img_data.reshape(
            [int(header["shape"][0]),
             int(header["shape"][1])])
        self.header = header

        self.rendered_image = self.render_image(self.selected_colormap,
                                                self.clip_min, self.clip_max)

    def render_image(self, colormap=None, clip_min=None, clip_max=None):
        """
        Render an image from the image data, applying a colormap to the greyscale data.

        :param colormap: Desired image colormap. if None, uses the default colormap.
        :param clip_min: The minimum pixel value desired. If a pixel is lower than this value,
        it is set to this value.
        :param clip_max: The maximum pixel value desired. If a pixel is higher than this value,
        it is set to this value.
        :return: The rendered image binary data, encoded into a string so it can be returned
        by a GET request.
        """
        if colormap is None:
            colormap = self.selected_colormap

        if clip_min is not None and clip_max is not None:
            if clip_min > clip_max:
                clip_min = None
                clip_max = None
                logging.warning(
                    "Clip minimum cannot be more than clip maximum")

        if clip_min is not None or clip_max is not None:
            img_clipped = np.clip(self.img_data, clip_min,
                                  clip_max)  # clip image

        else:
            img_clipped = self.img_data

        # Scale to 0-255 for colormap
        img_scaled = self.scale_array(img_clipped, 0,
                                      255).astype(dtype=np.uint8)

        # Apply colormap
        cv2_colormap = self.cv2_colormaps[self.colormap_options[colormap]]
        img_colormapped = cv2.applyColorMap(img_scaled, cv2_colormap)

        # Most time consuming step, depending on image size and the type of image
        img_encode = cv2.imencode('.png',
                                  img_colormapped,
                                  params=[cv2.IMWRITE_PNG_COMPRESSION, 0])[1]
        return img_encode.tostring()

    @staticmethod
    def scale_array(src, tmin, tmax):
        """
        Set the range of image data.

        The ratio between pixels should remain the same, but the total range should be rescaled
        to fit the desired minimum and maximum
        :param src: the source array to rescale
        :param tmin: the target minimum
        :param tmax: the target maximum
        :return: an array of the same dimensions as the source, but with the data rescaled.
        """
        smin, smax = src.min(), src.max()

        downscaled = (src.astype(float) - smin) / (smax - smin)
        rescaled = (downscaled * (tmax - tmin) + tmin).astype(src.dtype)

        return rescaled

    def convert_to_string(self, obj):
        """
        Convert all unicode parts of a dictionary or list to standard strings.

        This method may not handle special characters well!
        :param obj: the dictionary, list, or unicode string
        :return: the same data type as obj, but with unicode strings converted to python strings.
        """
        if isinstance(obj, dict):
            return {
                self.convert_to_string(key): self.convert_to_string(value)
                for key, value in obj.items()
            }
        elif isinstance(obj, list):
            return [self.convert_to_string(element) for element in obj]
        elif isinstance(obj, unicode):
            return obj.encode('utf-8')

        return obj

    def cleanup(self):
        """Close the IPC channels ready for shutdown."""
        for channel in self.ipc_channels:
            channel.cleanup()

    def get_selected_colormap(self):
        """
        Get the default colormap for the adapter.

        :return: the default colormap for the adapter
        """
        return self.selected_colormap

    def set_selected_colormap(self, colormap):
        """
        Set the selected colormap for the adapter.

        :param colormap: colormap to select
        """
        if colormap.lower() in self.colormap_options:
            self.selected_colormap = colormap.lower()

    def set_clip(self, clip_array):
        """
        Set the image clipping, i.e. max and min values to render.

        :param clip_array: array of min and max values to clip
        """
        if (clip_array[0] is None) or isinstance(clip_array[0], int):
            self.clip_min = clip_array[0]

        if (clip_array[1] is None) or isinstance(clip_array[1], int):
            self.clip_max = clip_array[1]

    def get_channel_endpoints(self):
        """
        Get the list of endpoints this adapter is subscribed to.

        :return: a list of endpoints
        """
        endpoints = []
        for channel in self.ipc_channels:
            endpoints.append(channel.endpoint)

        return endpoints

    def get_channel_counts(self):
        """
        Get a dict of the endpoints and the count of how many frames came from that endpoint.

        :return: A dict, with the endpoint as a key, and the number of images from that endpoint
        as the value
        """
        counts = {}
        for channel in self.ipc_channels:
            counts[channel.endpoint] = channel.frame_count

        return counts

    def set_channel_counts(self, data):
        """
        Set the channel frame counts.

        This method is used to reset the channel frame counts to known values.
        :param data: channel frame count data to set
        """
        data = self.convert_to_string(data)
        logging.debug("Data Type: %s", type(data).__name__)
        for channel in self.ipc_channels:
            if channel.endpoint in data:
                logging.debug("Endpoint %s in request", channel.endpoint)
                channel.frame_count = data[channel.endpoint]
Пример #17
0
class Workshop():
    """Workshop - class that extracts and stores information about system-level parameters."""

    # Thread executor used for background tasks
    executor = futures.ThreadPoolExecutor(max_workers=1)

    def __init__(self, background_task_enable, background_task_interval):
        """Initialise the Workshop object.

        This constructor initlialises the Workshop object, building a parameter tree and
        launching a background task if enabled
        """
        # Save arguments
        self.background_task_enable = background_task_enable
        self.background_task_interval = background_task_interval

        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Set the background task counters to zero
        self.background_ioloop_counter = 0
        self.background_thread_counter = 0

        # Build a parameter tree for the background task
        bg_task = ParameterTree({
            'ioloop_count': (lambda: self.background_ioloop_counter, None),
            'thread_count': (lambda: self.background_thread_counter, None),
            'enable':
            (lambda: self.background_task_enable, self.set_task_enable),
            'interval':
            (lambda: self.background_task_interval, self.set_task_interval),
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version':
            version_info['version'],
            'tornado_version':
            tornado.version,
            'server_uptime': (self.get_server_uptime, None),
            'background_task':
            bg_task
        })

        # Launch the background task if enabled in options
        if self.background_task_enable:
            self.start_background_tasks()

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the Workshop adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)

    def set(self, path, data):
        """Set parameters in the parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate WorkshopError.

        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise WorkshopError(e)

    def cleanup(self):
        """Clean up the Workshop instance.

        This method stops the background tasks, allowing the adapter state to be cleaned up
        correctly.
        """
        self.stop_background_tasks()

    def set_task_interval(self, interval):
        """Set the background task interval."""
        logging.debug("Setting background task interval to %f", interval)
        self.background_task_interval = float(interval)

    def set_task_enable(self, enable):
        """Set the background task enable."""
        enable = bool(enable)

        if enable != self.background_task_enable:
            if enable:
                self.start_background_tasks()
            else:
                self.stop_background_tasks()

    def start_background_tasks(self):
        """Start the background tasks."""
        logging.debug("Launching background tasks with interval %.2f secs",
                      self.background_task_interval)

        self.background_task_enable = True

        # Register a periodic callback for the ioloop task and start it
        self.background_ioloop_task = PeriodicCallback(
            self.background_ioloop_callback,
            self.background_task_interval * 1000)
        self.background_ioloop_task.start()

        # Run the background thread task in the thread execution pool
        self.background_thread_task()

    def stop_background_tasks(self):
        """Stop the background tasks."""
        self.background_task_enable = False
        self.background_ioloop_task.stop()

    def background_ioloop_callback(self):
        """Run the adapter background IOLoop callback.

        This simply increments the background counter before returning. It is called repeatedly
        by the periodic callback on the IOLoop.
        """

        if self.background_ioloop_counter < 10 or self.background_ioloop_counter % 20 == 0:
            logging.debug("Background IOLoop task running, count = %d",
                          self.background_ioloop_counter)

        self.background_ioloop_counter += 1

    @run_on_executor
    def background_thread_task(self):
        """The the adapter background thread task.

        This method runs in the thread executor pool, sleeping for the specified interval and 
        incrementing its counter once per loop, until the background task enable is set to false.
        """

        sleep_interval = self.background_task_interval

        while self.background_task_enable:
            time.sleep(sleep_interval)
            if self.background_thread_counter < 10 or self.background_thread_counter % 20 == 0:
                logging.debug("Background thread task running, count = %d",
                              self.background_thread_counter)
            self.background_thread_counter += 1

        logging.debug("Background thread task stopping")
Пример #18
0
class ProxyTarget(object):
    """
    Proxy adapter target class.

    This class implements a proxy target, its parameter tree and associated
    status information for use in the ProxyAdapter.
    """

    def __init__(self, name, url, request_timeout):
        """
        Initalise the ProxyTarget object.

        Sets up the default state of the target object, builds the
        appropriate parameter tree to be handled by the containing adapter
        and sets up the HTTP client for making requests to the target.
        """
        self.name = name
        self.url = url
        self.request_timeout = request_timeout

        # Initialise default state
        self.status_code = 0
        self.error_string = 'OK'
        self.last_update = 'unknown'
        self.data = {}
        self.counter = 0

        # Build a parameter tree representation of the proxy target status
        self.status_param_tree = ParameterTree({
            'url': (self._get_url, None),
            'status_code': (self._get_status_code, None),
            'error': (self._get_error_string, None),
            'last_update': (self._get_last_update, None),
        })

        # Build a parameter tree representation of the proxy target data
        self.data_param_tree = ParameterTree((self._get_data, None))

        # Create an HTTP client instance and set up default request headers
        self.http_client = tornado.httpclient.HTTPClient()
        self.request_headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        }

        self.remote_get()  # init the data tree

    def update(self, request, path):
        """
        Update the Proxy Target `ParameterTree` with data from the proxied adapter,
        after issuing a GET or a PUT request to it. It also updates the status code
        and error string if the HTTP request fails.
        """

        try:
            # Request data to/from the target
            response = self.http_client.fetch(request)

            # Update status code and data accordingly
            self.status_code = response.code
            self.error_string = 'OK'
            response_body = tornado.escape.json_decode(response.body)

        except tornado.httpclient.HTTPError as http_err:
            # Handle HTTP errors, updating status information and reporting error
            self.status_code = http_err.code
            self.error_string = http_err.message
            logging.error(
                "Proxy target %s fetch failed: %d %s",
                self.name,
                self.status_code,
                self.error_string
                )
            self.last_update = tornado.httputil.format_timestamp(time.time())
            return

        except IOError as other_err:
            self.status_code = 502
            self.error_string = str(other_err)
            logging.error(
                "Proxy target %s fetch failed: %d %s",
                self.name,
                self.status_code,
                self.error_string
                )
            self.last_update = tornado.httputil.format_timestamp(time.time())
            return

        data_ref = self.data  # reference for modification
        if path:
            # if the path exists, we need to split it so we can navigate the data
            path_elems = path.split('/')
            if path_elems[-1] == '':  # remove empty string caused by trailing slashes
                del path_elems[-1]
            for elem in path_elems[:-1]:
                # for each element, traverse down the data tree
                data_ref = data_ref[elem]

        for key in response_body:
            new_elem = response_body[key]
            data_ref[key] = new_elem
        logging.debug(
            "Proxy target %s fetch succeeded: %d %s",
            self.name,
            self.status_code,
            self.data_param_tree.get(path)
            )

        # Update the timestamp of the last request in standard format
        self.last_update = tornado.httputil.format_timestamp(time.time())

    def remote_get(self, path=''):
        """
        Get data from the remote target.

        This method updates the local proxy target with new data by
        issuing a GET request to the target URL, and then updates the proxy
        target data and status information according to the response.
        """

        # create request to PUT data, send to the target
        request = tornado.httpclient.HTTPRequest(
            url=self.url + path,
            method="GET",
            headers=self.request_headers,
            request_timeout=self.request_timeout
        )
        self.update(request, path)

    def remote_set(self, path, data):
        """
        Set data on the remote target.

        This method updates the local proxy target with new datat by
        issuing a PUT request to the target URL, and then updates the proxy
        target data and status information according to the response.
        """
        # create request to PUT data, send to the target
        request = tornado.httpclient.HTTPRequest(
            url=self.url + path,
            method="PUT",
            body=data,
            headers=self.request_headers,
            request_timeout=self.request_timeout
        )
        self.update(request, path)

    def _get_status_code(self):
        """
        Get the target request status code.

        This internal method is used to retrieve the status code
        of the last target update request for use in the parameter
        tree.
        """
        return self.status_code

    def _get_error_string(self):
        """
        Get the target request error string.

        This internal method is used to retrieve the error string
        of the last target update request for use in the parameter
        tree.
        """
        return self.error_string

    def _get_last_update(self):
        """
        Get the target request last update timestamp.

        This internal method is used to retrieve the timestamp
        of the last target update request for use in the parameter
        tree.
        """
        return self.last_update

    def _get_data(self):
        """
        Get the target request data.

        This internal method is used to retrieve the target updated during last call to update(),
        for use in the parameter tree.
        """
        return self.data

    def _get_url(self):
        return self.url
Пример #19
0
class Hexitec():
    """Hexitec: Class that extracts and stores information about system-level parameters."""

    # Thread executor used for background tasks
    thread_executor = futures.ThreadPoolExecutor(max_workers=3)

    def __init__(self, options):
        """Initialise the Hexitec object.

        This constructor initialises the Hexitec object, building a
        parameter tree and launching a background task if enabled
        """
        defaults = HexitecDetectorDefaults()
        self.file_dir = options.get("save_dir", defaults.save_dir)
        self.file_name = options.get("save_file", defaults.save_file)
        self.number_frames = options.get("acquisition_num_frames",
                                         defaults.number_frames)
        self.number_frames_to_request = self.number_frames
        self.total_delay = 0.0
        # Backup number_frames as first initialisation temporary sets number_frames = 2
        self.backed_up_number_frames = self.number_frames

        self.duration = 1
        self.duration_enable = False

        self.daq = HexitecDAQ(self, self.file_dir, self.file_name)

        self.adapters = {}

        self.fem = None
        for key, value in options.items():
            if "fem" in key:
                fem_info = value.split(',')
                fem_info = [(i.split('=')[0], i.split('=')[1])
                            for i in fem_info]
                fem_dict = {
                    fem_key.strip(): fem_value.strip()
                    for (fem_key, fem_value) in fem_info
                }
                logging.debug(fem_dict)
                self.fem = HexitecFem(
                    self,
                    fem_dict.get("server_ctrl_ip_addr",
                                 defaults.fem["server_ctrl_ip"]),
                    fem_dict.get("camera_ctrl_ip_addr",
                                 defaults.fem["camera_ctrl_ip"]),
                    fem_dict.get("server_data_ip_addr",
                                 defaults.fem["server_data_ip"]),
                    fem_dict.get("camera_data_ip_addr",
                                 defaults.fem["camera_data_ip"]))

        if not self.fem:
            self.fem = HexitecFem(
                parent=self,
                server_ctrl_ip_addr=defaults.fem["server_ctrl_ip"],
                camera_ctrl_ip_addr=defaults.fem["camera_ctrl_ip"],
                server_data_ip_addr=defaults.fem["server_data_ip"],
                camera_data_ip_addr=defaults.fem["camera_data_ip"])

        self.fem_health = True

        # Bias (clock) tracking variables #
        self.bias_clock_running = False
        self.bias_init_time = 0  # Placeholder
        self.bias_blocking_acquisition = False
        self.extended_acquisition = False  # Track acquisition spanning bias window(s)
        self.frames_already_acquired = 0  # Track frames acquired across collection windows

        self.collect_and_bias_time = self.fem.bias_refresh_interval + \
            self.fem.bias_voltage_settle_time + self.fem.time_refresh_voltage_held

        # Tracks whether first acquisition of multiple, bias-window(s), collection
        self.initial_acquisition = True
        # Tracks whether 2 frame fudge collection: (during cold initialisation)
        self.first_initialisation = True

        self.acquisition_in_progress = False

        # Watchdog variables
        self.error_margin = 400  # TODO: Revisit timeouts
        self.fem_tx_timeout = 5000
        self.daq_rx_timeout = self.collect_and_bias_time + self.error_margin
        self.fem_start_timestamp = 0
        self.time_waiting_for_data_arrival = 0

        # Store initialisation time
        self.init_time = time.time()

        self.system_health = True
        self.status_message = ""
        self.status_error = ""
        self.elog = ""
        self.number_nodes = 1
        # Software states:
        #   Cold, Disconnected, Idle, Acquiring
        self.software_state = "Cold"
        self.cold_initialisation = True

        detector = ParameterTree({
            "fem":
            self.fem.param_tree,
            "daq":
            self.daq.param_tree,
            "connect_hardware": (None, self.connect_hardware),
            "initialise_hardware": (None, self.initialise_hardware),
            "disconnect_hardware": (None, self.disconnect_hardware),
            "collect_offsets": (None, self._collect_offsets),
            "commit_configuration": (None, self.commit_configuration),
            "software_state": (lambda: self.software_state, None),
            "cold_initialisation": (lambda: self.cold_initialisation, None),
            "hv_on": (None, self.hv_on),
            "hv_off": (None, self.hv_off),
            "acquisition": {
                "number_frames":
                (lambda: self.number_frames, self.set_number_frames),
                "duration": (lambda: self.duration, self.set_duration),
                "duration_enable":
                (lambda: self.duration_enable, self.set_duration_enable),
                "start_acq": (None, self.acquisition),
                "stop_acq": (None, self.cancel_acquisition)
            },
            "status": {
                "system_health": (lambda: self.system_health, None),
                "status_message": (lambda: self.status_message, None),
                "status_error": (lambda: self.status_error, None),
                "elog": (lambda: self.elog, self.set_elog),
                "fem_health": (lambda: self.fem_health, None),
                "number_nodes":
                (lambda: self.number_nodes, self.set_number_nodes)
            }
        })

        self.system_info = SystemInfo()

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            "system_info": self.system_info.param_tree,
            "detector": detector
        })

        self._start_polling()

    def _start_polling(self):
        IOLoop.instance().add_callback(self.polling)

    def polling(self):  # pragma: no cover
        """Poll FEM for status.

        Check if acquisition completed (if initiated), for error(s) and
        whether DAQ/FEM watchdogs timed out.
        """
        # Poll FEM acquisition & health status
        self.poll_fem()

        # Watchdog: Watch FEM in case no data from hardware triggered by fem.acquire_data()
        self.check_fem_watchdog()

        # TODO: WATCHDOG, monitor HexitecDAQ rate of frames_processed updated.. (Break if stalled)
        self.check_daq_watchdog()

        IOLoop.instance().call_later(1.0, self.polling)

    def get_frames_processed(self):
        """Get number of frames processed across node(s)."""
        status = self._get_od_status("fp")
        frames_processed = 0
        for index in status:
            # rank = index.get('hdf', None).get('rank')
            # frames = index.get('histogram').get('frames_processed')
            # print("    g_f_p(), rank: {} frames_processed: {}".format(rank, frames))
            frames_processed = frames_processed + index.get('histogram').get(
                'frames_processed')
        return frames_processed

    def poll_fem(self):
        """Poll FEM for acquisition and health status."""
        if self.fem.acquisition_completed:
            frames_processed = self.get_frames_processed()
            # Either cold initialisation (first_initialisation is True, therefore only 2 frames
            # expected) or, ordinary collection (self.number_frames frames expected)
            if ((self.first_initialisation and (frames_processed == 2))
                    or (frames_processed == self.number_frames)):  # noqa: W503

                if self.first_initialisation:
                    self.first_initialisation = False
                    self.number_frames = self.backed_up_number_frames  # TODO: redundant

                # Reset FEM's acquisiton status ahead of future acquisitions
                self.fem.acquisition_completed = False
        # TODO: Also check sensor values?
        # ..
        fem_health = self.fem.get_health()
        self.fem_health = fem_health
        if self.system_health:
            self.status_error = self.fem._get_status_error()
            self.status_message = self.fem._get_status_message()
            self.system_health = self.system_health and self.fem_health

    def check_fem_watchdog(self):
        """Check data sent when FEM acquiring data."""
        if self.acquisition_in_progress:
            # TODO: Monitor FEM in case no data following fem.acquire_data() call
            if (self.fem.hardware_busy):
                fem_begun = self.fem.acquire_timestamp
                delta_time = time.time() - fem_begun
                logging.debug("    FEM w-dog: {0:.2f} < {1:.2f}".format(
                    delta_time, self.fem_tx_timeout))
                if (delta_time > self.fem_tx_timeout):
                    self.fem.stop_acquisition = True
                    self.shutdown_processing()
                    logging.error("FEM data transmission timed out")
                    error = "Timed out waiting ({0:.2f} seconds) for FEM data".format(
                        delta_time)
                    self.fem._set_status_message(error)

    def check_daq_watchdog(self):
        """Monitor DAQ's frames_processed while data processed.

        Ensure frames_processed increments, completes within reasonable time of acquisition.
        Failure to do so indicate missing/dropped packet(s), stop processing if stalled.
        """
        if self.daq.in_progress:
            processed_timestamp = self.daq.processed_timestamp
            delta_time = time.time() - processed_timestamp
            if (delta_time > self.daq_rx_timeout):
                logging.error("    DAQ -- PROCESSING TIMED OUT")
                # DAQ: Timed out waiting for next frame to process
                self.shutdown_processing()
                logging.error(
                    "DAQ processing timed out; Saw %s expected %s frames" %
                    (self.daq.frames_processed,
                     self.daq.frame_end_acquisition))
                self.fem._set_status_error(
                    "Processing timed out: {0:.2f} seconds \
                    (exceeded {1:.2f}); Expected {2} got {3} frames\
                        ".format(delta_time, self.daq_rx_timeout,
                                 self.daq.frame_end_acquisition,
                                 self.daq.frames_processed))
                self.fem._set_status_message("Processing abandoned")

    def shutdown_processing(self):
        """Stop processing in DAQ."""
        self.daq.shutdown_processing = True
        self.acquisition_in_progress = False

    def _get_od_status(self, adapter):
        """Get status from adapter."""
        try:
            request = ApiAdapterRequest(None, content_type="application/json")
            response = self.adapters[adapter].get("status", request)
            response = response.data["value"]
        except KeyError:
            logging.warning("%s Adapter Not Found" % adapter)
            response = [{"Error": "Adapter {} not found".format(adapter)}]
        finally:
            return response

    def connect_hardware(self, msg):
        """Set up watchdog timeout, start bias clock and connect with hardware."""
        # TODO: Must recalculate collect and bias time both here and in initialise()
        #   Logically, commit_configuration() is the best place but it updates variables before
        #   reading .ini file
        self.collect_and_bias_time = self.fem.bias_refresh_interval + \
            self.fem.bias_voltage_settle_time + self.fem.time_refresh_voltage_held

        self.daq_rx_timeout = self.collect_and_bias_time + self.error_margin
        # Start bias clock if not running
        if not self.bias_clock_running:
            IOLoop.instance().add_callback(self.start_bias_clock)
        self.fem.connect_hardware(msg)
        self.software_state = "Idle"

    def start_bias_clock(self):
        """Set up bias 'clock'."""
        if not self.bias_clock_running:
            self.bias_init_time = time.time()
            self.bias_clock_running = True
        self.poll_bias_clock()

    def poll_bias_clock(self):
        """Call periodically (0.1 seconds often enough??) to bias window status.

        Are we in bias refresh intv /  refresh volt held / Settle time ?
        Example: 60000 / 3000 / 2000: Collect for 60s, pause for 3+2 secs
        """
        current_time = time.time()
        time_elapsed = current_time - self.bias_init_time
        if (time_elapsed < self.fem.bias_refresh_interval):
            # Still within collection window - acquiring data is allowed
            pass
        else:
            if (time_elapsed < self.collect_and_bias_time):
                # Blackout period - Wait for electrons to replenish/voltage to stabilise
                self.bias_blocking_acquisition = True
            else:
                # Beyond blackout period - Back within bias
                # Reset bias clock
                self.bias_init_time = current_time
                self.bias_blocking_acquisition = False

        IOLoop.instance().call_later(0.1, self.poll_bias_clock)

    def initialise_hardware(self, msg):
        """Initialise hardware.

        Recalculate collect and bias timing, update watchdog timeout.
        """
        # TODO: Must recalculate collect and bias time both here and in initialise();
        #   Logically, commit_configuration() is the best place but it updates variables before
        #   values read from .ini file
        self.collect_and_bias_time = self.fem.bias_refresh_interval + \
            self.fem.bias_voltage_settle_time + self.fem.time_refresh_voltage_held

        self.daq_rx_timeout = self.collect_and_bias_time + self.error_margin
        # If first initialisation, ie fudge, temporarily change number_frames to 2
        # Adapter also controls this change in FEM
        if self.first_initialisation:
            self.backed_up_number_frames = self.number_frames
            self.number_frames = 2
            # TODO: Fix this fudge?
            self.fem.acquire_timestamp = time.time()
            self.acquisition_in_progress = True
        self.fem.initialise_hardware(msg)
        # Wait for fem initialisation/fudge frames
        IOLoop.instance().call_later(0.5, self.monitor_fem_progress)

    def disconnect_hardware(self, msg):
        """Disconnect FEM's hardware connection."""
        if self.daq.in_progress:
            # Stop hardware if still in acquisition
            if self.fem.hardware_busy:
                self.cancel_acquisition()
            # Reset daq
            self.shutdown_processing()
            # Allow processing to shutdown before disconnecting hardware
            IOLoop.instance().call_later(0.2, self.fem.disconnect_hardware)
        else:
            # Nothing in progress, disconnect hardware
            self.fem.disconnect_hardware(msg)
        self.software_state = "Disconnected"
        # Reset system status
        self.status_error = ""
        self.status_message = ""
        self.system_health = True
        # Stop bias clock
        if self.bias_clock_running:
            self.bias_clock_running = False

    def set_duration_enable(self, duration_enable):
        """Set duration enable, calculating number of frames accordingly."""
        self.duration_enable = duration_enable
        self.fem.set_duration_enable(duration_enable)
        # Ensure DAQ, FEM have correct duration/number of frames configured
        if duration_enable:
            self.set_duration(self.duration)
        else:
            # print("\n\tadp.set_duration_enable({}) number_frames: {}\n".format(duration_enable, self.number_frames))
            self.set_number_frames(self.number_frames)

    def set_number_frames(self, frames):
        """Set number of frames in DAQ, FEM."""
        # print("\n\tadp.set_number_frames({}) -> number_frames: {}\n".format(frames, self.number_frames))
        self.number_frames = frames
        # Update number of frames in Hardware, and (via DAQ) in histogram and hdf plugins
        self.fem.set_number_frames(self.number_frames)
        self.daq.set_number_frames(self.number_frames)

    def set_duration(self, duration):
        """Set duration, calculate frames from frame rate and update DAQ, FEM."""
        self.duration = duration
        self.fem.set_duration(self.duration)
        # print("\n\tadp.set_duration({}) number_frames {} -> {}\n".format(duration, self.fem.get_number_frames(), self.number_frames))
        self.number_frames = self.fem.get_number_frames()
        self.daq.set_number_frames(self.number_frames)

    def set_elog(self, entry):
        """Set the elog entry provided by the user through the UI."""
        self.elog = entry

    def set_number_nodes(self, number_nodes):
        """Set number of nodes."""
        self.number_nodes = number_nodes
        self.daq.set_number_nodes(self.number_nodes)

    def initialize(self, adapters):
        """Get references to adapters, and pass these to the classes that need to use them."""
        self.adapters = dict(
            (k, v) for k, v in adapters.items() if v is not self)
        self.daq.initialize(self.adapters)

    def acquisition(self, put_data=None):
        """Instruct DAQ and FEM to acquire data."""
        # Synchronise first_initialisation status (i.e. collect 2 fudge frames) with FEM
        if self.first_initialisation:
            self.first_initialisation = self.fem.first_initialisation
        else:
            # Clear (any previous) daq error
            self.daq.in_error = False

        if self.extended_acquisition is False:
            if self.daq.in_progress:
                logging.warning("Cannot Start Acquistion: Already in progress")
                self.fem._set_status_error(
                    "Cannot Start Acquistion: Already in progress")
                return

        self.total_delay = 0
        self.number_frames_to_request = self.number_frames

        if self.fem.bias_voltage_refresh:
            # Did the acquisition coincide with bias dead time?
            if self.bias_blocking_acquisition:
                IOLoop.instance().call_later(0.1, self.acquisition)
                return

            # Work out how many frames can be acquired before next bias refresh
            time_into_window = time.time() - self.bias_init_time
            time_available = self.fem.bias_refresh_interval - time_into_window

            if time_available < 0:
                IOLoop.instance().call_later(0.09, self.acquisition)
                return

            frames_before_bias = self.fem.frame_rate * time_available
            number_frames_before_bias = int(round(frames_before_bias))

            self.number_frames_to_request = self.number_frames - self.frames_already_acquired

            # Can we obtain all required frames within current bias window?
            if (number_frames_before_bias < self.number_frames_to_request):
                # Need >1 bias window to fulfil acquisition
                self.extended_acquisition = True
                self.number_frames_to_request = number_frames_before_bias

            self.total_delay = time_available + self.fem.bias_voltage_settle_time + \
                self.fem.time_refresh_voltage_held

        # # TODO: Remove once Firmware made to reset on each new acquisition
        # # TODO: WILL BE NON 0 VALUE IN THE FUTURE - TO SUPPORT BIAS REFRESH INTV
        # #       BUT, if nonzero then won't FP's Acquisition time out before processing done?????
        # #
        # Reset Reorder plugin's frame_number (to current frame number, for multi-window acquire)
        command = "config/reorder/frame_number"
        request = ApiAdapterRequest(self.file_dir,
                                    content_type="application/json")
        request.body = "{}".format(self.frames_already_acquired)
        self.adapters["fp"].put(command, request)
        # TODO: To be removed once firmware updated? FP may be slow to process frame_number reset
        time.sleep(0.5)

        # Reset histograms, call DAQ's prepare_daq() once per acquisition
        if self.initial_acquisition:
            # Issue reset to histogram
            command = "config/histogram/reset_histograms"
            request = ApiAdapterRequest(self.file_dir,
                                        content_type="application/json")
            request.body = "{}".format(1)
            self.adapters["fp"].put(command, request)

            self.daq_target = time.time()
            self.daq.prepare_daq(self.number_frames)
            self.initial_acquisition = False
            # Acquisition (whether single/multi-run) starts here
            self.acquisition_in_progress = True

        # Wait for DAQ (i.e. file writer) to be enabled before FEM told to collect data
        # IOLoop.instance().call_later(0.1, self.await_daq_ready)
        IOLoop.instance().add_callback(self.await_daq_ready)

    def await_daq_ready(self):
        """Wait until DAQ has configured, enabled file writer."""
        if (self.daq.in_error):
            # Reset state variables
            self.reset_state_variables()
        elif (self.daq.file_writing is False):
            IOLoop.instance().call_later(0.05, self.await_daq_ready)
        else:
            self.software_state = "Acquiring"
            # Add additional 8 ms delay to ensure file writer's file open before first frame arrives
            IOLoop.instance().call_later(0.08, self.trigger_fem_acquisition)

    def trigger_fem_acquisition(self):
        """Trigger data acquisition in fem."""
        # TODO: Temp hack: Prevent frames being 1 (continuous readout) by setting to 2 if it is
        self.number_frames_to_request = 2 if (self.number_frames_to_request == 1) else \
            self.number_frames_to_request
        self.fem.set_number_frames(self.number_frames_to_request)
        self.fem.collect_data()

        self.frames_already_acquired += self.number_frames_to_request
        # Note when FEM told to begin collecting data
        self.fem_start_timestamp = time.time()
        IOLoop.instance().call_later(self.total_delay,
                                     self.monitor_fem_progress)

    def monitor_fem_progress(self):
        """Check fem hardware progress.

        Busy either:
        -Initialising from cold (2 fudge frames)
        -Normal initialisation
        -Waiting for data collection to complete, either single/multi run
        """
        if (self.fem.hardware_busy):
            # Still sending data
            IOLoop.instance().call_later(0.5, self.monitor_fem_progress)
            return
        else:
            # Current collection completed; Do we have all the requested frames?
            if self.extended_acquisition:
                if (self.frames_already_acquired < self.number_frames):
                    # Need further bias window(s)
                    IOLoop.instance().add_callback(self.acquisition)
                    return

        # Issue reset to summed_image
        command = "config/summed_image/reset_image"
        request = ApiAdapterRequest(self.file_dir,
                                    content_type="application/json")
        request.body = "{}".format(1)
        self.adapters["fp"].put(command, request)

        rc = self.daq.prepare_odin()
        if not rc:
            message = "Prepare Odin failed!"
            self.fem._set_status_error(message)
            self.status_error = message

        self.reset_state_variables()

    def reset_state_variables(self):
        """Reset state variables.

        Utilised by await_daq_ready(), monitor_fem_progress()
        """
        self.initial_acquisition = True
        self.extended_acquisition = False
        self.acquisition_in_progress = False
        self.frames_already_acquired = 0
        self.software_state = "Idle"

    def cancel_acquisition(self, put_data=None):
        """Cancel ongoing acquisition in Software.

        Not yet possible to stop FEM, mid-acquisition
        """
        self.fem.stop_acquisition = True
        # Inject End of Acquisition Frame
        command = "config/inject_eoa"
        request = ApiAdapterRequest("", content_type="application/json")
        self.adapters["fp"].put(command, request)
        self.shutdown_processing()
        self.software_state = "Idle"

    def _collect_offsets(self, msg):
        """Instruct FEM to collect offsets."""
        self.fem.collect_offsets()

    def commit_configuration(self, msg):
        """Push HexitecDAQ's 'config/' ParameterTree settings into FP's plugins."""
        self.daq.commit_configuration()
        # Clear cold initialisation if first config commit
        if self.cold_initialisation:
            self.cold_initialisation = False

    def hv_on(self, msg):
        """Switch HV on."""
        # TODO: Complete placeholder
        self.fem.hv_bias_enabled = True

    def hv_off(self, msg):
        """Switch HV off."""
        # TODO: Complete placeholder
        self.fem.hv_bias_enabled = False

    def get(self, path):
        """
        Get the parameter tree.

        This method returns the parameter tree for use by clients via the Hexitec adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)

    def set(self, path, data):
        """
        Set parameters in the parameter tree.

        This method simply wraps underlying ParameterTree method so that an exception can be
        re-raised with an appropriate HexitecError.

        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise HexitecError(e)
Пример #20
0
class Workshop():
    """Workshop - class that extracts and stores information about system-level parameters."""

    # Thread executor used for background tasks
    executor = futures.ThreadPoolExecutor(max_workers=1)

    def __init__(self, background_task_enable, background_task_interval):
        """Initialise the Workshop object.

        This constructor initlialises the Workshop object, building a parameter tree and
        launching a background task if enabled
        """
        # Save arguments
        self.background_task_enable = background_task_enable
        self.background_task_interval = background_task_interval

        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Build a parameter tree for the background task
        bg_task = ParameterTree({
            'count': (lambda: self.background_task_counter, None),
            'enable':
            (lambda: self.background_task_enable, self.set_task_enable),
            'interval':
            (lambda: self.background_task_interval, self.set_task_interval),
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version':
            version_info['version'],
            'tornado_version':
            tornado.version,
            'server_uptime': (self.get_server_uptime, None),
            'background_task':
            bg_task
        })

        # Set the background task counter to zero
        self.background_task_counter = 0

        # Launch the background task if enabled in options
        if self.background_task_enable:
            logging.debug("Launching background task with interval %.2f secs",
                          background_task_interval)
            self.background_task()

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the Workshop adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)

    def set(self, path, data):
        """Set parameters in the parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate WorkshopError.

        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise WorkshopError(e)

    def set_task_interval(self, interval):

        logging.debug("Setting background task interval to %f", interval)
        self.background_task_interval = float(interval)

    def set_task_enable(self, enable):

        logging.debug("Setting background task enable to %s", enable)

        current_enable = self.background_task_enable
        self.background_task_enable = bool(enable)

        if not current_enable:
            logging.debug("Restarting background task")
            self.background_task()

    @run_on_executor
    def background_task(self):
        """Run the adapter background task.

        This simply increments the background counter and sleeps for the specified interval,
        before adding itself as a callback to the IOLoop instance to be called again.

        """
        if self.background_task_counter < 10 or self.background_task_counter % 20 == 0:
            logging.debug("Background task running, count = %d",
                          self.background_task_counter)

        self.background_task_counter += 1
        time.sleep(self.background_task_interval)

        if self.background_task_enable:
            IOLoop.instance().add_callback(self.background_task)
        else:
            logging.debug("Background task no longer enabled, stopping")
Пример #21
0
class ProxyAdapter(ApiAdapter):
    """
    Proxy adapter class for ODIN server.

    This class implements a proxy adapter, allowing ODIN server to forward requests to
    other HTTP services.
    """

    def __init__(self, **kwargs):
        """
        Initialise the ProxyAdapter.

        This constructor initialises the adapter instance, parsing configuration
        options out of the keyword arguments it is passed. A ProxyTarget object is
        instantiated for each target specified in the options.

         :param kwargs: keyword arguments specifying options
        """

        # Initialise base class
        super(ProxyAdapter, self).__init__(**kwargs)

        # Set the HTTP request timeout if present in the options
        request_timeout = None
        if TIMEOUT_CONFIG_NAME in self.options:
            try:
                request_timeout = float(self.options[TIMEOUT_CONFIG_NAME])
                logging.debug('ProxyAdapter request timeout set to %f secs', request_timeout)
            except ValueError:
                logging.error(
                    "Illegal timeout specified for ProxyAdapter: %s",
                    self.options[TIMEOUT_CONFIG_NAME]
                    )

        # Parse the list of target-URL pairs from the options, instantiating a ProxyTarget
        # object for each target specified.
        self.targets = []
        if TARGET_CONFIG_NAME in self.options:
            for target_str in self.options[TARGET_CONFIG_NAME].split(','):
                try:
                    (target, url) = target_str.split('=')
                    self.targets.append(ProxyTarget(target.strip(), url.strip(), request_timeout))
                except ValueError:
                    logging.error("Illegal target specification for ProxyAdapter: %s",
                                  target_str.strip())

        # Issue an error message if no targets were loaded
        if self.targets:
            logging.debug("ProxyAdapter with {:d} targets loaded".format(len(self.targets)))
        else:
            logging.error("Failed to resolve targets for ProxyAdapter")

        # Construct the parameter tree returned by this adapter
        tree = {'status': {}}
        for target in self.targets:
            tree['status'][target.name] = target.status_param_tree
            tree[target.name] = target.data_param_tree

        self.param_tree = ParameterTree(tree)

    @request_types('application/json')
    @response_types('application/json', default='application/json')
    def get(self, path, request):
        """
        Handle an HTTP GET request.

        This method handles an HTTP GET request, returning a JSON response.

        :param path: URI path of request
        :param request: HTTP request object
        :return: an ApiAdapterResponse object containing the appropriate response
        """
        # Update the target specified in the path, or all targets if none specified
        if "/" in path:
            path_elem, target_path = path.split('/', 1)
        else:
            path_elem = path
            target_path = ""
        for target in self.targets:
            if path_elem == '' or path_elem == target.name:
                target.remote_get(target_path)

        # Build the response from the adapter parameter tree
        try:
            response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as param_tree_err:
            response = {'error': str(param_tree_err)}
            status_code = 400

        return ApiAdapterResponse(response, status_code=status_code)

    @request_types("application/json", "application/vnd.odin-native")
    @response_types('application/json', default='application/json')
    def put(self, path, request):
        """
        Handle an HTTP PUT request.

        This method handles an HTTP PUT request, returning a JSON response.

        :param path: URI path of request
        :param request: HTTP request object
        :return: an ApiAdapterResponse object containing the appropriate response
        """
        # Update the target specified in the path, or all targets if none specified

        try:
            json_decode(request.body)  # ensure request body is JSON. Will throw a TypeError if not
            if "/" in path:
                path_elem, target_path = path.split('/', 1)
            else:
                path_elem = path
                target_path = ""
            for target in self.targets:
                if path_elem == '' or path_elem == target.name:
                    target.remote_set(target_path, request.body)
            response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as param_tree_err:
            response = {'error': str(param_tree_err)}
            status_code = 400
        except (TypeError, ValueError) as type_val_err:
            response = {'error': 'Failed to decode PUT request body: {}'.format(str(type_val_err))}
            status_code = 415

        return ApiAdapterResponse(response, status_code=status_code)
Пример #22
0
class Workshop():
    """Workshop - class that extracts and stores information about system-level parameters."""

    # Thread executor used for background tasks
    executor = futures.ThreadPoolExecutor(max_workers=1)

    # Setting up pins
    RED = 2
    YELLOW = 1
    GREEN = 0

    def __init__(self, LED_task_enable, LED_task_interval, temp_task_enable):
        """Initialise the Workshop object.

        This constructor initlialises the Workshop object, building a parameter tree and
        launching a background task if enabled
        """

        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Initialise MCP23008 device
        self.mcp = MCP23008(address=0x20, busnum=2)
        num_pins = 3
        for pin in range(num_pins):
            self.mcp.setup(pin, MCP23008.OUT)
            self.mcp.output(pin, 0)
        self.led_states = [0] * num_pins

        # Set up thermocouple instance and variables
        self.thermoC = Max31856()
        self.avg_temp = 0
        self.avg_temp_calc = [0] * 10
        self.avg_count = 0
        self.ten_count_switch = False
        self.temp_task_enable = temp_task_enable
        self.temp_bounds = [21.50, 22.00]

        # Save LED_task arguments
        self.task_mode = 'command'
        self.LED_task_enable = LED_task_enable
        self.LED_task_interval = LED_task_interval

        # Set the background task counters to zero
        self.rave_ioloop_counter = 0
        self.traffic_wait_counter = 0
        self.traffic_loop_counter = 0
        self.temp_count = 0
        self.background_thread_counter = 0  # not using the thread

        # Tell user default mode for LEDs
        logging.debug('LED mode set to default: {}.'.format(self.task_mode))

        # Build a parameter tree for the background task
        LED_task = ParameterTree({
            'rave_count': (lambda: self.rave_ioloop_counter, None),
            'traffic_count': (lambda: self.traffic_loop_counter, None),
            'enable': (lambda: self.LED_task_enable, self.LED_task_enable),
            'task_mode':
            (lambda: self.task_mode, lambda mode: self.set_task_mode(mode)),
            'interval':
            (lambda: self.LED_task_interval, self.set_LED_task_interval),
        })

        # A parameter tree for the LEDs to interact with
        led_tree = ParameterTree({
            'red': (lambda: self.led_states[self.RED],
                    lambda state: self.set_led_state(self.RED, state)),
            'yellow': (lambda: self.led_states[self.YELLOW],
                       lambda state: self.set_led_state(self.YELLOW, state)),
            'green': (lambda: self.led_states[self.GREEN],
                      lambda state: self.set_led_state(self.GREEN, state)),
        })

        # Sub-tree to change temperature boundaries
        thermo_bound_tree = ParameterTree({
            'lower': (lambda: self.temp_bounds[0],
                      lambda temp: self.set_temp_bounds(0, temp)),
            'upper': (lambda: self.temp_bounds[1],
                      lambda temp: self.set_temp_bounds(1, temp))
        })

        # Parameter tree for the thermocouple
        thermo_tree = ParameterTree({
            'temperature': (lambda: self.thermoC.temperature, None),
            'rolling_avg': (lambda: self.avg_temp, None),
            'temps_counted': (lambda: self.temp_count, None),
            'temp_bounds':
            thermo_bound_tree,
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'odin_version':
            version_info['version'],
            'tornado_version':
            tornado.version,
            'server_uptime': (self.get_server_uptime, None),
            'LED_task':
            LED_task,
            'leds':
            led_tree,
            'temperature':
            thermo_tree,
        })

        # Launch the background tasks if enabled in options
        if self.LED_task_enable:
            self.start_LED_task()

        if self.temp_task_enable:
            self.start_temp_task()

    def set_task_mode(self, mode):
        logging.debug('setting task mode to {}'.format(mode))
        self.task_mode = str(mode)

    def set_led_state(self, led, state):
        self.led_states[led] = int(state)
        logging.info('Setting LED {} state to {}'.format(led, state))
        self.mcp.output(led, state)

    def set_temp_bounds(self, bound, temp):
        self.temp_bounds[bound] = temp
        if bound == 0:
            bound = 'Lower'
        elif bound == 1:
            bound = 'Upper'
        else:
            print('Invalid bound provided. Should not be seen.')
            pass

        logging.info('{} bound set to {}'.format(bound, temp))

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the Workshop adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path)

    def set(self, path, data):
        """Set parameters in the parameter tree.

        This method simply wraps underlying ParameterTree method so that an exceptions can be
        re-raised with an appropriate WorkshopError.

        :param path: path of parameter tree to set values for
        :param data: dictionary of new data values to set in the parameter tree
        """
        try:
            self.param_tree.set(path, data)
        except ParameterTreeError as e:
            raise WorkshopError(e)

    def cleanup(self):
        """Clean up the Workshop instance.

        This method stops the background tasks, allowing the adapter state to be cleaned up
        correctly.
        """
        self.stop_LED_task()

########

    def set_LED_task_interval(self, interval):
        """Set the background task interval."""
        logging.debug("Setting background task interval to %f", interval)
        self.LED_task_interval = float(interval)

    def set_LED_task_enable(self, enable):
        """Set the background task enable."""
        enable = bool(enable)

        if enable != self.LED_task_enable:
            if enable:
                self.start_LED_task()
            else:
                self.stop_LED_task()
########

    def start_LED_task(self):
        """Start the background tasks."""
        logging.debug("Launching background tasks with interval %.2f secs",
                      self.LED_task_interval)
        self.LED_task_enable = True

        # Register a periodic callback for the ioloop task and start it
        self.LED_ioloop_task = PeriodicCallback(self.LED_ioloop_callback,
                                                self.LED_task_interval * 1000)
        self.LED_ioloop_task.start()

        # Run the background thread task in the thread execution pool
#        self.background_thread_task()

    def stop_LED_task(self):
        """Stop the background tasks."""
        self.LED_task_enable = False
        self.LED_ioloop_task.stop()

    def update_led(self, led, state):
        '''A function to turn an LED on, and to update its state in led_states,
           to save on code duplication and in case another theoretical device
           wants to be used.'''
        self.mcp.output(led, state)
        self.led_states[led] = int(state)

    def LED_ioloop_callback(self):
        '''Run the LED ioloop callback. It should randomly switch LEDS off and on
           whenever it is called, it won't always just switch them.'''

        # RAVE task
        if self.task_mode == 'rave':
            for i in range(3):
                self.update_led(random.randint(0, 2), random.randint(0, 1))
            self.rave_ioloop_counter += 1

        # Traffic task
        if self.task_mode == 'traffic':
            self.traffic_wait_counter += 1

            if self.traffic_wait_counter == 2:  # 1 added when called
                self.update_led(self.YELLOW, 0)  # assuming interval=0.25s
                self.update_led(self.RED, 1)

            elif self.traffic_wait_counter == 14:  # 2+12 (+3s default)
                self.update_led(self.YELLOW, 1)

            elif self.traffic_wait_counter == 22:  # 14+8 (+2s)
                self.update_led(self.RED, 0)
                self.update_led(self.YELLOW, 0)
                self.update_led(self.GREEN, 1)

            elif self.traffic_wait_counter == 34:  # 22+12 (+3s)
                self.update_led(self.GREEN, 0)
                self.update_led(self.YELLOW, 1)

            elif self.traffic_wait_counter == 39:  # 36+3, +1 added waiting to start over
                self.traffic_wait_counter = 0  # 2s on this one
                self.traffic_loop_counter += 1
        # Thermometer and command mode
        else:  # Both are handled here. Command has no task, and
            pass  # it makes more sense to put thermometer with temp_task

    def start_temp_task(self):
        """Start the thermocouple task."""
        self.temp_task_enable = True

        self.temp_ioloop_task = PeriodicCallback(
            self.temp_ioloop_callback, 1000
        )  # Interval set to 1 second, no reason to add a variable interval.

        self.temp_ioloop_task.start()

    def stop_temp_task(self):
        """Stop the thermocouple task."""
        self.temp_task_enable = False
        self.temp_ioloop_task.stop()

    def temp_ioloop_callback(self):
        """Thermocouple callback task.
           Once per second, read the temperature.
           If in the correct mode, interact with the LEDs from here as well.
        """
        print("Thermocouple temperature is {:.1f} C".format(
            self.thermoC.temperature))
        temperature = self.thermoC.temperature

        #Calculating the rolling average
        self.avg_temp = 0
        self.avg_temp_calc[self.avg_count] = temperature
        self.avg_count += 1
        for temp in self.avg_temp_calc:
            self.avg_temp += temp

        if self.ten_count_switch:
            self.avg_temp /= 10
            if self.avg_count == 10:  # count still needs reset at 10
                self.avg_count = 0
        else:
            if self.avg_count < 10:
                self.avg_temp /= self.avg_count
            else:
                self.ten_count_switch = True  # once 10 temps recorded
                self.avg_temp /= 10  # always divide by 10 for avg
                self.avg_count = 0

        self.temp_count += 1

        if self.task_mode == 'thermometer':

            self.update_led(self.RED, 0)  # Turn LEDs off so that
            self.update_led(self.YELLOW, 0)  # only one is on at once
            self.update_led(self.GREEN, 0)

            if temperature < self.temp_bounds[0]:  # < Lower bound
                self.update_led(self.YELLOW, 1)
            elif temperature < self.temp_bounds[
                    1] and temperature > self.temp_bounds[0]:
                # elif lower < temp < upper
                self.update_led(self.GREEN, 1)
            elif temperature > self.temp_bounds[1]:
                self.update_led(self.RED, 1)

    @run_on_executor
    def background_thread_task(self):
        """The the adapter background thread task.

        This method runs in the thread executor pool, sleeping for the specified interval and 
        incrementing its counter once per loop, until the background task enable is set to false.
        """

        sleep_interval = self.background_task_interval

        while self.background_task_enable:
            time.sleep(sleep_interval)
            if self.background_thread_counter < 10 or self.background_thread_counter % 20 == 0:
                logging.debug("Background thread task running, count = %d",
                              self.background_thread_counter)
            self.background_thread_counter += 1

        logging.debug("Background thread task stopping")
Пример #23
0
class ProxyAdapter(ApiAdapter):
    """
    Proxy adapter class for ODIN server.

    This class implements a proxy adapter, allowing ODIN server to forward requests to
    other HTTP services.
    """
    def __init__(self, **kwargs):
        """
        Initialise the ProxyAdapter.

        This constructor initialises the adapter instance, parsing configuration
        options out of the keyword arguments it is passed. A ProxyTarget object is
        instantiated for each target specified in the options.

         :param kwargs: keyword arguments specifying options
        """

        # Initialise base class
        super(ProxyAdapter, self).__init__(**kwargs)

        # Set the HTTP request timeout if present in the options
        request_timeout = None
        if TIMEOUT_CONFIG_NAME in self.options:
            try:
                request_timeout = float(self.options[TIMEOUT_CONFIG_NAME])
                logging.debug('ProxyAdapter request timeout set to %f secs',
                              request_timeout)
            except ValueError:
                logging.error("Illegal timeout specified for ProxyAdapter: %s",
                              self.options[TIMEOUT_CONFIG_NAME])

        # Parse the list of target-URL pairs from the options, instantiating a ProxyTarget
        # object for each target specified.
        self.targets = []
        if TARGET_CONFIG_NAME in self.options:
            for target_str in self.options[TARGET_CONFIG_NAME].split(','):
                try:
                    (target, url) = target_str.split('=')
                    self.targets.append(
                        ProxyTarget(target.strip(), url.strip(),
                                    request_timeout))
                except ValueError:
                    logging.error(
                        "Illegal target specification for ProxyAdapter: %s",
                        target_str.strip())

        # Issue an error message if no targets were loaded
        if self.targets:
            logging.debug("ProxyAdapter with {:d} targets loaded".format(
                len(self.targets)))
        else:
            logging.error("Failed to resolve targets for ProxyAdapter")

        status_dict = {}
        # Construct the parameter tree returned by this adapter
        tree = {}
        meta_tree = {}
        for target in self.targets:
            status_dict[target.name] = target.status_param_tree

            tree[target.name] = target.data_param_tree
            meta_tree[target.name] = target.meta_param_tree

        self.status_tree = ParameterTree(status_dict)
        tree['status'] = self.status_tree
        meta_tree['status'] = self.status_tree.get("", True)

        self.param_tree = ParameterTree(tree)
        self.meta_param_tree = ParameterTree(meta_tree)

    @response_types('application/json', default='application/json')
    def get(self, path, request):
        """
        Handle an HTTP GET request.

        This method handles an HTTP GET request, returning a JSON response.

        :param path: URI path of request
        :param request: HTTP request object
        :return: an ApiAdapterResponse object containing the appropriate response
        """

        get_metadata = wants_metadata(request)
        # Update the target specified in the path, or all targets if none specified
        if "/" in path:
            path_elem, target_path = path.split('/', 1)
        else:
            path_elem = path
            target_path = ""
        for target in self.targets:
            if path_elem == "" or path_elem == target.name:
                target.remote_get(target_path, get_metadata)

        # Build the response from the adapter parameter tree
        try:
            if get_metadata:
                if path_elem == "" or path_elem == "status":
                    # update status tree with metadata
                    self.meta_param_tree.set('status',
                                             self.status_tree.get("", True))
                response = self.meta_param_tree.get(path)

            else:
                response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as param_tree_err:
            response = {'error': str(param_tree_err)}
            status_code = 400

        return ApiAdapterResponse(response, status_code=status_code)

    @request_types("application/json", "application/vnd.odin-native")
    @response_types('application/json', default='application/json')
    def put(self, path, request):
        """
        Handle an HTTP PUT request.

        This method handles an HTTP PUT request, returning a JSON response.

        :param path: URI path of request
        :param request: HTTP request object
        :return: an ApiAdapterResponse object containing the appropriate response
        """
        # Update the target specified in the path, or all targets if none specified

        try:
            body = decode_request_body(
                request
            )  # ensure request body is JSON. Will throw a TypeError if not
            if "/" in path:
                path_elem, target_path = path.split('/', 1)
            else:
                path_elem = path
                target_path = ""
            for target in self.targets:
                if path_elem == '' or path_elem == target.name:
                    target.remote_set(target_path, body)

            response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as param_tree_err:
            response = {'error': str(param_tree_err)}
            status_code = 400
        except (TypeError, ValueError) as type_val_err:
            response = {
                'error':
                'Failed to decode PUT request body: {}'.format(
                    str(type_val_err))
            }
            status_code = 415

        return ApiAdapterResponse(response, status_code=status_code)
Пример #24
0
class BaseProxyAdapter(object):
    """
    Proxy adapter base mixin class.

    This mixin class implements the core functionality required by all concrete proxy adapter
    implementations.
    """
    TIMEOUT_CONFIG_NAME = 'request_timeout'
    TARGET_CONFIG_NAME = 'targets'

    def initialise_proxy(self, proxy_target_cls):
        """
        Initialise the proxy.

        This method initialises the proxy. The adapter options are parsed to determine the list
        of proxy targets and request timeout, then a proxy target of the specified class is created
        for each target. The data, metadata and status structures and parameter trees associated
        with each target are created.

        :param proxy_target_cls: proxy target class appropriate for the specific implementation
        """
        # Set the HTTP request timeout if present in the options
        request_timeout = None
        if self.TIMEOUT_CONFIG_NAME in self.options:
            try:
                request_timeout = float(self.options[self.TIMEOUT_CONFIG_NAME])
                logging.debug('Proxy adapter request timeout set to %f secs', request_timeout)
            except ValueError:
                logging.error(
                    "Illegal timeout specified for proxy adapter: %s",
                    self.options[self.TIMEOUT_CONFIG_NAME]
                )

        # Parse the list of target-URL pairs from the options, instantiating a proxy target of the
        # specified type for each target specified.
        self.targets = []
        if self.TARGET_CONFIG_NAME in self.options:
            for target_str in self.options[self.TARGET_CONFIG_NAME].split(','):
                try:
                    (target, url) = target_str.split('=')
                    self.targets.append(
                        proxy_target_cls(target.strip(), url.strip(), request_timeout)
                    )
                except ValueError:
                    logging.error("Illegal target specification for proxy adapter: %s",
                                  target_str.strip())

        # Issue an error message if no targets were loaded
        if self.targets:
            logging.debug("Proxy adapter with {:d} targets loaded".format(len(self.targets)))
        else:
            logging.error("Failed to resolve targets for proxy adapter")

        # Build the parameter trees implemented by this adapter for the specified proxy targets
        status_dict = {}
        tree = {}
        meta_tree = {}

        for target in self.targets:
            status_dict[target.name] = target.status_param_tree
            tree[target.name] = target.data_param_tree
            meta_tree[target.name] = target.meta_param_tree

        # Create a parameter tree from the status data for the targets and insert into the
        # data and metadata structures
        self.status_tree = ParameterTree(status_dict)
        tree['status'] = self.status_tree
        meta_tree['status'] = self.status_tree.get("", True)

        # Create the data and metadata parameter trees
        self.param_tree = ParameterTree(tree)
        self.meta_param_tree = ParameterTree(meta_tree)

    def proxy_get(self, path, get_metadata):
        """
        Get data from the proxy targets.

        This method gets data from one or more specified targets and returns the responses.

        :param path: path to data on remote targets
        :param get_metadata: flag indicating if metadata is to be requested
        :return: list of target responses
        """
        # Resolve the path element and target path
        path_elem, target_path = self._resolve_path(path)

        # Iterate over the targets and get data if the path matches
        target_responses = []
        for target in self.targets:
            if path_elem == "" or path_elem == target.name:
                target_responses.append(target.remote_get(target_path, get_metadata))

        return target_responses

    def proxy_set(self, path, data):
        """
        Set data on the proxy targets.

        This method sets data on one or more specified targets and returns the responses.

        :param path: path to data on remote targets
        :param data to set on targets
        :return: list of target responses
        """
        # Resolve the path element and target path
        path_elem, target_path = self._resolve_path(path)

        # Iterate over the targets and set data if the path matches
        target_responses = []
        for target in self.targets:
            if path_elem == '' or path_elem == target.name:
                target_responses.append(target.remote_set(target_path, data))

        return target_responses

    def _resolve_response(self, path, get_metadata=False):
        """
        Resolve the response to a proxy target get or set request.

        This method resolves the appropriate response to a proxy target get or set request. Data
        or metadata from the specified path is returned, along with an appropriate HTTP status code.

        :param path: path to data on remote targets
        :param get_metadata: flag indicating if metadata is to be requested

        """
        # Build the response from the adapter parameter trees, matching to the path for one or more
        # targets
        try:
            # If metadata is requested, update the status tree with metadata before returning
            # metadata
            if get_metadata:
                path_elem, _ = self._resolve_path(path)
                if path_elem in ("", "status"):
                    # update status tree with metadata
                    self.meta_param_tree.set('status', self.status_tree.get("", True))
                response = self.meta_param_tree.get(path)
            else:
                response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as param_tree_err:
            response = {'error': str(param_tree_err)}
            status_code = 400

        return (response, status_code)

    @staticmethod
    def _resolve_path(path):
        """
        Resolve the specified path into a path element and target.

        This method resolves the specified path into a path element and target path.

        :param path: path to data on remote targets
        :return: tuple of path element and target path
        """
        if "/" in path:
            path_elem, target_path = path.split('/', 1)
        else:
            path_elem = path
            target_path = ""
        return (path_elem, target_path)
Пример #25
0
class SystemInfo(with_metaclass(Singleton, object)):
    """SystemInfo - class that extracts and stores information about system-level parameters."""

    # __metaclass__ = Singleton

    def __init__(self):
        """Initialise the SystemInfo object.

        This constructor initlialises the SystemInfo object, extracting various system-level
        parameters and storing them in a parameter tree to be accessible to clients.
        """
        # Store initialisation time
        self.init_time = time.time()

        # Get package version information
        version_info = get_versions()

        # Extract platform information and store in parameter tree
        (system, node, release, version, _, processor) = platform.uname()
        platform_tree = ParameterTree({
            'name':
            'platform',
            'description':
            "Information about the underlying platform",
            'system': (lambda: system, {
                "name": "system",
                "description": "operating system name",
            }),
            'node': (lambda: node, {
                "name": "node",
                "description": "node (host) name",
            }),
            'release': (lambda: release, {
                "name": "release",
                "description": "operating system release",
            }),
            'version': (lambda: version, {
                "name": "version",
                "description": "operating system version",
            }),
            'processor': (lambda: processor, {
                "name": "processor",
                "description": "processor (CPU) name",
            }),
        })

        # Store all information in a parameter tree
        self.param_tree = ParameterTree({
            'name':
            'system_info',
            'description':
            'Information about the system hosting this odin server instance',
            'odin_version': (lambda: version_info['version'], {
                "name": "odin version",
                "description": "ODIN server version",
            }),
            'tornado_version': (lambda: tornado.version, {
                "name":
                "tornado version",
                "description":
                "version of tornado used in this server",
            }),
            'python_version': (lambda: platform.python_version(), {
                "name":
                "python version",
                "description":
                "version of python running this server",
            }),
            'platform':
            platform_tree,
            'server_uptime': (self.get_server_uptime, {
                "name": "server uptime",
                "description": "time since the ODIN server started",
                "units": "s",
                "display_precision": 2,
            }),
        })

    def get_server_uptime(self):
        """Get the uptime for the ODIN server.

        This method returns the current uptime for the ODIN server.
        """
        return time.time() - self.init_time

    def get(self, path, with_metadata=False):
        """Get the parameter tree.

        This method returns the parameter tree for use by clients via the SystemInfo adapter.

        :param path: path to retrieve from tree
        """
        return self.param_tree.get(path, with_metadata)