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
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)
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
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)
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)
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
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)
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 = []
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()
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)}
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
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()
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
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)
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]
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")
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
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)
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")
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)
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")
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)
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)