def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.CLOCK, r'(.*)\r\n', lambda match: match.group(1), lambda string: str(string), type=ParameterDictType.STRING, display_name="clock", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add( Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE)
def __init__(self, prompts, newline, driver_event, connections=None): """ Constructor. @param prompts Enum class containing possible device prompts used for command response logic. @param newline The device newline. @driver_event The callback for asynchronous driver events. """ if not type(connections) is list: raise InstrumentProtocolException( 'Unable to instantiate multi connection protocol without connection list' ) self._param_dict2 = ProtocolParameterDict() # Construct superclass. WorkhorseProtocol.__init__(self, prompts, newline, driver_event) # Create multiple connection versions of the pieces of protocol involving data to/from the instrument self._linebuf = {connection: '' for connection in connections} self._promptbuf = {connection: '' for connection in connections} self._last_data_timestamp = { connection: None for connection in connections } self.connections = {connection: None for connection in connections} self.chunkers = { connection: StringChunker(self.sieve_function) for connection in connections }
def _set_params(self, *args, **kwargs): if len(args) < 1: raise InstrumentParameterException( 'Set command requires a parameter dict.') params = args[0] if not isinstance(params, dict): raise InstrumentParameterException('Set parameters not a dict.') self._param_dict = ProtocolParameterDict() for param in params: log.info('Creating new parameter: %s', param) self._param_dict.add(param, '', None, None) self._param_dict.set_value(param, params[param])
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.CLOCK, r'(.*)\r\n', lambda match : match.group(1), lambda string : str(string), type=ParameterDictType.STRING, display_name="clock", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add(Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE)
def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("baz", r'.*baz=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS) self.param_dict.add("bat", r'.*bat=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY)
def __init__(self, driver_event): """ Base constructor. @param driver_event The callback for asynchronous driver events. """ # Event callback to send asynchronous events to the agent. self._driver_event = driver_event # The connection used to talk to the device. self._connection = None # The protocol state machine. self._protocol_fsm = None # The parameter dictionary. self._param_dict = ProtocolParameterDict() # The spot to stash a configuration before going into direct access # mode self._pre_direct_access_config = None # Driver configuration passed from the user self._startup_config = {} # scheduler config is a bit redundant now, but if we ever want to # re-initialize a scheduler we will need it. self._scheduler = None self._scheduler_callback = {} self._scheduler_config = {}
def __init__(self, config, memento, data_callback, state_callback, exception_callback): self._config = config self._data_callback = data_callback self._state_callback = state_callback self._exception_callback = exception_callback self._memento = memento self._publisher_thread = None self._verify_config() self._param_dict = ProtocolParameterDict() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._build_param_dict()
def _set_params(self, *args, **kwargs): if len(args) < 1: raise InstrumentParameterException("Set command requires a parameter dict.") params = args[0] if not isinstance(params, dict): raise InstrumentParameterException("Set parameters not a dict.") self._param_dict = ProtocolParameterDict() for param in params: log.info("Creating new parameter: %s", param) self._param_dict.add(param, "", None, None) self._param_dict.set_value(param, params[param])
def __init__(self, config, memento, data_callback, state_callback, exception_callback): self._config = config self._data_callback = data_callback self._state_callback = state_callback self._exception_callback = exception_callback self._memento = memento self._verify_config() self._param_dict = ProtocolParameterDict() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._build_param_dict()
def __init__(self, driver_event): """ Base constructor. @param driver_event The callback for asynchronous driver events. """ # Event callback to send asynchronous events to the agent. self._driver_event = driver_event # The connection used to talk to the device. self._connection = None # The protocol state machine. self._protocol_fsm = None # The parameter dictionary. self._param_dict = ProtocolParameterDict()
def __init__(self, prompts, newline, driver_event, connections=None): """ Constructor. @param prompts Enum class containing possible device prompts used for command response logic. @param newline The device newline. @driver_event The callback for asynchronous driver events. """ if not type(connections) is list: raise InstrumentProtocolException('Unable to instantiate multi connection protocol without connection list') self._param_dict2 = ProtocolParameterDict() # Construct superclass. WorkhorseProtocol.__init__(self, prompts, newline, driver_event) # Create multiple connection versions of the pieces of protocol involving data to/from the instrument self._linebuf = {connection: '' for connection in connections} self._promptbuf = {connection: '' for connection in connections} self._last_data_timestamp = {connection: None for connection in connections} self.connections = {connection: None for connection in connections} self.chunkers = {connection: StringChunker(self.sieve_function) for connection in connections}
def __init__(self, config, memento, data_callback, state_callback, event_callback, exception_callback): self._config = copy.deepcopy(config) self._data_callback = data_callback self._state_callback = state_callback self._event_callback = event_callback self._exception_callback = exception_callback self._memento = memento self._publisher_thread = None self._verify_config() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._resource_id = None self._param_dict = ProtocolParameterDict() self._cmd_dict = ProtocolCommandDict() self._driver_dict = DriverDict() self._build_command_dict() self._build_driver_dict() self._build_param_dict()
class Protocol(SamiProtocol): """ Instrument protocol class Subclasses SamiProtocol and CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. SamiProtocol.__init__(self, prompts, newline, driver_event) ## Continue building protocol state machine from SamiProtocol self._protocol_fsm.add_handler( ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler( ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) self._protocol_fsm.add_handler( ProtocolState.POLLED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler( ProtocolState.POLLED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) # State state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # build the chunker bot self._chunker = StringChunker(Protocol.sieve_function) @staticmethod def sieve_function(raw_data): """ The method that splits samples """ return_list = [] sieve_matchers = [REGULAR_STATUS_REGEX_MATCHER, CONTROL_RECORD_REGEX_MATCHER, SAMI_SAMPLE_REGEX_MATCHER, CONFIGURATION_REGEX_MATCHER, ERROR_REGEX_MATCHER] for matcher in sieve_matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ self._extract_sample(SamiRegularStatusDataParticle, REGULAR_STATUS_REGEX_MATCHER, chunk, timestamp) self._extract_sample(SamiControlRecordDataParticle, CONTROL_RECORD_REGEX_MATCHER, chunk, timestamp) self._extract_sample(PhsenSamiSampleDataParticle, SAMI_SAMPLE_REGEX_MATCHER, chunk, timestamp) self._extract_sample(PhsenConfigDataParticle, CONFIGURATION_REGEX_MATCHER, chunk, timestamp) ######################################################################### ## General (for POLLED and SCHEDULED states) Sample handlers. ######################################################################### def _handler_sample_success(self, *args, **kwargs): next_state = None result = None return (next_state, result) def _handler_sample_timeout(self, ): next_state = None result = None return (next_state, result) #################################################################### # Build Parameter dictionary #################################################################### def _build_param_dict(self): """ Populate the parameter dictionary with parameters. For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() self._param_dict.add(Parameter.LAUNCH_TIME, CONFIGURATION_REGEX, lambda match: int(match.group(1), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='launch time') self._param_dict.add(Parameter.START_TIME_FROM_LAUNCH, CONFIGURATION_REGEX, lambda match: int(match.group(2), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02C7EA00, visibility=ParameterDictVisibility.READ_ONLY, display_name='start time after launch time') self._param_dict.add(Parameter.STOP_TIME_FROM_START, CONFIGURATION_REGEX, lambda match: int(match.group(3), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01E13380, visibility=ParameterDictVisibility.READ_ONLY, display_name='stop time after start time') self._param_dict.add(Parameter.MODE_BITS, CONFIGURATION_REGEX, lambda match: int(match.group(4), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0A, visibility=ParameterDictVisibility.READ_ONLY, display_name='mode bits (set to 00001010)') self._param_dict.add(Parameter.SAMI_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(5), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami sample interval') self._param_dict.add(Parameter.SAMI_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(6), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x04, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami driver version') self._param_dict.add(Parameter.SAMI_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(7), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami parameter pointer') self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(8), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 sample interval') self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(9), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 driver version') self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(10), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0B, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 parameter pointer') self._param_dict.add(Parameter.DEVICE2_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(11), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 sample interval') self._param_dict.add(Parameter.DEVICE2_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(12), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 driver version') self._param_dict.add(Parameter.DEVICE2_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(13), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 parameter pointer') self._param_dict.add(Parameter.DEVICE3_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(14), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 sample interval') self._param_dict.add(Parameter.DEVICE3_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(15), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 driver version') self._param_dict.add(Parameter.DEVICE3_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(16), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 parameter pointer') self._param_dict.add(Parameter.PRESTART_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(17), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart sample interval') self._param_dict.add(Parameter.PRESTART_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(18), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart driver version') self._param_dict.add(Parameter.PRESTART_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(19), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart parameter pointer') self._param_dict.add(Parameter.GLOBAL_CONFIGURATION, CONFIGURATION_REGEX, lambda match: int(match.group(20), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='global bits (set to 00000111)') self._param_dict.add(Parameter.NUMBER_SAMPLES_AVERAGED, CONFIGURATION_REGEX, lambda match: int(match.group(21), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_FLUSHES, CONFIGURATION_REGEX, lambda match: int(match.group(22), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(23), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(24), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_REAGENT_PUMPS, CONFIGURATION_REGEX, lambda match: int(match.group(25), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.VALVE_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(26), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_IND, CONFIGURATION_REGEX, lambda match: int(match.group(27), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PV_OFF_IND, CONFIGURATION_REGEX, lambda match: int(match.group(28), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_BLANKS, CONFIGURATION_REGEX, lambda match: int(match.group(29), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_MEASURE_T, CONFIGURATION_REGEX, lambda match: int(match.group(30), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_TO_MEASURE, CONFIGURATION_REGEX, lambda match: int(match.group(31), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.MEASURE_TO_PUMP_ON, CONFIGURATION_REGEX, lambda match: int(match.group(32), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_MEASUREMENTS, CONFIGURATION_REGEX, lambda match: int(match.group(33), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.SALINITY_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(34), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='')
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct) # Add build handlers for device commands. self._add_build_handler(Command.BATTERY, self._build_simple_command) # Add response handlers for device commands. self._add_response_handler(Command.BATTERY, self._parse_battery_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(RASFL_SampleDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) """ if return_list != []: log.debug("sieve_function: raw_data=%s, return_list=%s" %(raw_data, return_list)) """ return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s" %chunk) self._extract_sample(RASFL_SampleDataParticle, RASFL_SampleDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # implement virtual methods from base class. ######################################################################## def apply_startup_params(self): """ Apply sample_interval startup parameter. """ config = self.get_startup_config() log.debug("apply_startup_params: startup config = %s" %config) if config.has_key(Parameter.SAMPLE_INTERVAL): log.debug("apply_startup_params: setting sample_interval to %d" %config[Parameter.SAMPLE_INTERVAL]) self._param_dict.set_value(Parameter.SAMPLE_INTERVAL, config[Parameter.SAMPLE_INTERVAL]) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ # force to command mode, this instrument has no autosample mode next_state = ProtocolState.COMMAND result = ResourceAgentState.COMMAND log.debug("_handler_unknown_discover: state = %s", next_state) return (next_state, result) ######################################################################## # Command handlers. # just implemented to make DA possible, instrument has no actual command mode ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ # Command device to update parameters and send a config change event if needed. self._update_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ next_state = None result = None return (next_state, result) def _handler_command_start_direct(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.DIRECT_ACCESS next_agent_state = ResourceAgentState.DIRECT_ACCESS return (next_state, (next_agent_state, result)) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ pass def _handler_direct_access_execute_direct(self, data): next_state = None result = None self._do_cmd_direct(data) return (next_state, result) def _handler_direct_access_stop_direct(self): result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND return (next_state, (next_agent_state, result)) ######################################################################## # general handlers. ######################################################################## def _handler_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None time_format = "%Y/%m/%d %H:%M:%S" str_val = get_timestamp_delayed(time_format) log.debug("Setting instrument clock to '%s'", str_val) self._do_cmd_resp(Command.STOP, expected_prompt=Prompt.STOPPED) try: self._do_cmd_resp(Command.SET_CLOCK, str_val, expected_prompt=Prompt.CR_NL) finally: # ensure that we try to start the instrument sampling again self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO) return (next_state, (next_agent_state, result)) ######################################################################## # Private helpers. ######################################################################## def _wakeup(self, wakeup_timeout=10, response_timeout=3): """ Over-ridden because waking this instrument up is a multi-step process with two different requests required @param wakeup_timeout The timeout to wake the device. @param response_timeout The time to look for response to a wakeup attempt. @throw InstrumentTimeoutException if the device could not be woken. """ sleep_time = .1 command = Command.END_OF_LINE # Grab start time for overall wakeup timeout. starttime = time.time() while True: # Clear the prompt buffer. log.debug("_wakeup: clearing promptbuf: %s" % self._promptbuf) self._promptbuf = '' # Send a command and wait delay amount for response. log.debug('_wakeup: Sending command %s, delay=%s' %(command.encode("hex"), response_timeout)) for char in command: self._connection.send(char) time.sleep(INTER_CHARACTER_DELAY) sleep_amount = 0 while True: time.sleep(sleep_time) if self._promptbuf.find(Prompt.COMMAND_INPUT) != -1: # instrument is awake log.debug('_wakeup: got command input prompt %s' % Prompt.COMMAND_INPUT) # add inter-character delay which _do_cmd_resp() incorrectly doesn't add to the start of a transmission time.sleep(INTER_CHARACTER_DELAY) return Prompt.COMMAND_INPUT if self._promptbuf.find(Prompt.ENTER_CTRL_C) != -1: command = Command.CONTROL_C break if self._promptbuf.find(Prompt.PERIOD) == 0: command = Command.CONTROL_C break sleep_amount += sleep_time if sleep_amount >= response_timeout: log.debug("_wakeup: expected response not received, buffer=%s" % self._promptbuf) break if time.time() > starttime + wakeup_timeout: raise InstrumentTimeoutException("_wakeup(): instrument failed to wakeup in %d seconds time" %wakeup_timeout) def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="synchronize clock") def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.BATTERY, r'Battery: (.*)V', lambda match : match.group(1), lambda string : str(string), type=ParameterDictType.STRING, display_name="battery", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add(Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE) def _do_cmd_resp(self, cmd, *args, **kwargs): CommandResponseInstrumentProtocol._do_cmd_resp(self, cmd, args, kwargs, write_delay=INTER_CHARACTER_DELAY) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ log.debug("_update_params:") self._do_cmd_resp(Command.BATTERY) def _parse_battery_response(self, response, prompt): """ Parse handler for battery command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if clock command misunderstood. """ log.debug("_parse_battery_response: response=%s, prompt=%s" %(response, prompt)) if prompt == Prompt.UNRECOGNIZED_COMMAND: raise InstrumentProtocolException('battery command not recognized: %s.' % response) if not self._param_dict.update(response): raise InstrumentProtocolException('battery command not parsed: %s.' % response) return
class InstrumentProtocol(object): """ Base instrument protocol class. """ def __init__(self, driver_event): """ Base constructor. @param driver_event The callback for asynchronous driver events. """ # Event callback to send asynchronous events to the agent. self._driver_event = driver_event # The connection used to talk to the device. self._connection = None # The protocol state machine. self._protocol_fsm = None # The parameter dictionary. self._param_dict = ProtocolParameterDict() ######################################################################## # Helper methods ######################################################################## def got_data(self, data): """ Called by the instrument connection when data is available. Defined in subclasses. """ pass def get_current_state(self): """ Return current state of the protocol FSM. """ return self._protocol_fsm.get_current_state() def get_resource_capabilities(self, current_state=True): """ """ res_cmds = self._protocol_fsm.get_events(current_state) res_cmds = self._filter_capabilities(res_cmds) res_params = self._param_dict.get_keys() return [res_cmds, res_params] def _filter_capabilities(self, events): """ """ return events ######################################################################## # Command build and response parse handlers. ######################################################################## def _add_response_handler(self, cmd, func, state=None): """ Insert a handler class responsible for handling the response to a command sent to the instrument, optionally available only in a specific state. @param cmd The high level key of the command to respond to. @param func The function that handles the response @param state The state to pair with the command for which the function should be used """ if state == None: self._response_handlers[cmd] = func else: self._response_handlers[(state, cmd)] = func def _add_build_handler(self, cmd, func): """ Add a command building function. @param cmd The device command to build. @param func The function that constructs the command. """ self._build_handlers[cmd] = func ######################################################################## # Helpers to build commands. ######################################################################## def _build_simple_command(self, cmd, *args): """ Builder for simple commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s%s" % (cmd, self.eoln) def _build_keypress_command(self, cmd, *args): """ Builder for simple, non-EOLN-terminated commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s" % (cmd) def _build_multi_keypress_command(self, cmd, *args): """ Builder for simple, non-EOLN-terminated commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s%s%s%s%s%s" % (cmd, cmd, cmd, cmd, cmd, cmd) ######################################################################## # Static helpers to format set commands. ######################################################################## @staticmethod def _true_false_to_string(v): """ Write a boolean value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v a boolean value. @retval A yes/no string formatted as a Python boolean for set operations. @throws InstrumentParameterException if value not a bool. """ if not isinstance(v,bool): raise InstrumentParameterException('Value %s is not a bool.' % str(v)) return str(v) @staticmethod def _int_to_string(v): """ Write an int value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v An int val. @retval an int string formatted for generic set operations. @throws InstrumentParameterException if value not an int. """ if not isinstance(v,int): raise InstrumentParameterException('Value %s is not an int.' % str(v)) else: return '%i' % v @staticmethod def _float_to_string(v): """ Write a float value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v A float val. @retval a float string formatted for "generic" set operations. @throws InstrumentParameterException if value is not a float. """ if not isinstance(v,float): raise InstrumentParameterException('Value %s is not a float.' % v) else: return '%e' % v
class Protocol(McLaneProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. McLaneProtocol.__init__(self, prompts, newline, driver_event) # TODO - reset next_port on mechanical refresh of the PPS filters - how is the driver notified? # TODO - need to persist state for next_port to save driver restart self.next_port = 1 # next available port # get the following: # - VERSION # - CAPACITY (pump flow) # - BATTERY # - CODES (termination codes) # - COPYRIGHT (termination codes) def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ filling = self.get_current_state() == ProtocolState.FILL log.debug("_got_chunk:\n%s", chunk) sample_dict = self._extract_sample( PPSDNSampleDataParticle, PPSDNSampleDataParticle.regex_compiled(), chunk, timestamp, publish=filling ) if sample_dict: self._linebuf = "" self._promptbuf = "" self._protocol_fsm.on_event( ProtocolEvent.PUMP_STATUS, PPSDNSampleDataParticle.regex_compiled().search(chunk) ) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add( Parameter.FLUSH_VOLUME, r"Flush Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Flush Volume", description="Amount of sea water to flush prior to taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FLUSH_FLOWRATE, r"Flush Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Flush Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FLUSH_MINFLOW, r"Flush Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Flush Minimum Flow", description="If the flow rate falls below this value the flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_VOLUME, r"Fill Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Fill Volume", description="Amount of seawater to run through the collection filter (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_FLOWRATE, r"Fill Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Fill Flow Rate", description="Flow rate during sampling: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_MINFLOW, r"Fill Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Fill Minimum Flow", description="If the flow rate falls below this value the fill will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_VOLUME, r"Reverse Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Clear Volume", description="Amount of sea water to flush the home port after taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_FLOWRATE, r"Reverse Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Clear Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_MINFLOW, r"Reverse Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Clear Minimum Flow", description="If the flow rate falls below this value the reverse flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)
def test_empty_schema(self): self.param_dict = ProtocolParameterDict() result = self.param_dict.generate_dict() self.assertEqual(result, {})
class TestUnitProtocolParameterDict(TestUnitStringsDict): """ Test cases for instrument driver class. Functions in this class provide instrument driver unit tests and provide a tutorial on use of the driver interface. """ __test__ = True @staticmethod def pick_byte2(input_val): """ Get the 2nd byte as an example of something tricky and arbitrary""" val = int(input_val) >> 8 val &= 255 return val def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("baz", r'.*baz=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS, get_timeout=30, set_timeout=40, display_name="Baz", description="The baz parameter", type=ParameterDictType.INT, units="nano-bazers", value_description="Should be an integer between 2 and 2000") self.param_dict.add("bat", r'.*bat=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY, get_timeout=10, set_timeout=20, display_name="Bat", description="The bat parameter", type=ParameterDictType.INT, units="nano-batbit", value_description="Should be an integer between 1 and 1000") self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("pho", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("dil", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("qut", r'.*qut=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=[10, 100], visibility=ParameterDictVisibility.DIRECT_ACCESS, expiration=1, get_timeout=10, set_timeout=20, display_name="Qut", description="The qut list parameter", type=ParameterDictType.LIST, units="nano-qutters", value_description="Should be a 2-10 element list of integers between 2 and 2000") self.target_schema = { "bar": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 15 }, "visibility": "READ_WRITE", "range": None, }, "bat": { "description": "The bat parameter", "direct_access": False, "display_name": "Bat", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": 20, "description": "Should be an integer between 1 and 1000", "type": "int", "units": "nano-batbit" }, "visibility": "READ_ONLY", "range": None, }, "baz": { "description": "The baz parameter", "direct_access": True, "display_name": "Baz", "get_timeout": 30, "set_timeout": 40, "startup": False, "value": { "default": 20, "description": "Should be an integer between 2 and 2000", "type": "int", "units": "nano-bazers" }, "visibility": "DIRECT_ACCESS", "range": None, }, "dil": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "foo": { "direct_access": True, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 10 }, "visibility": "READ_WRITE", "range": None, }, "pho": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "qut": { "description": "The qut list parameter", "direct_access": True, "display_name": "Qut", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": [ 10, 100 ], "description": "Should be a 2-10 element list of integers between 2 and 2000", "type": "list", "units": "nano-qutters" }, "visibility": "DIRECT_ACCESS", "range": None, }, "qux": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "READ_ONLY", "range": None, } } self.test_yaml = ''' parameters: { qut: { description: "QutFileDesc", units: "QutFileUnits", value_description: "QutFileValueDesc", type: "QutFileType", display_name: "QutDisplay" }, extra_param: { description: "ExtraFileDesc", units: "ExtraFileUnits", value_description: "ExtraFileValueDesc", type: "ExtraFileType" } } commands: { dummy: stuff } ''' def test_get_direct_access_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_direct_access_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 3) self.assert_("foo" in result) self.assert_("baz" in result) self.assert_("qut" in result) def test_get_startup_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_startup_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 2) self.assert_("foo" in result) self.assert_("bar" in result) def test_set_default(self): """ Test setting a default value """ result = self.param_dict.get_config() self.assertEquals(result["foo"], None) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.param_dict.update("foo=1000") self.assertEquals(self.param_dict.get("foo"), 1000) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.assertRaises(ValueError, self.param_dict.set_default, "qux") def test_update_many(self): """ Test updating of multiple variables from the same input """ sample_input = """ foo=100 bar=200, baz=300 """ self.assertNotEquals(self.param_dict.get("foo"), 100) self.assertNotEquals(self.param_dict.get("bar"), 200) self.assertNotEquals(self.param_dict.get("baz"), 300) result = self.param_dict.update_many(sample_input) log.debug("result: %s", result) self.assertEquals(result["foo"], True) self.assertEquals(result["bar"], True) self.assertEquals(result["baz"], True) self.assertEquals(self.param_dict.get("foo"), 100) self.assertEquals(self.param_dict.get("bar"), 200) self.assertEquals(self.param_dict.get("baz"), 300) def test_update_specific_values(self): """ test to verify we can limit update to a specific set of parameters """ sample_input = "foo=100, bar=200" # First verify we can set both self.assertNotEquals(self.param_dict.get("foo"), 100) self.assertNotEquals(self.param_dict.get("bar"), 200) self.assertTrue(self.param_dict.update(sample_input)) self.assertEquals(self.param_dict.get("foo"), 100) self.assertEquals(self.param_dict.get("bar"), 200) # Now let's only have it update 1 parameter with a name sample_input = "foo=200, bar=300" self.assertTrue(self.param_dict.update(sample_input, target_params="foo")) self.assertEquals(self.param_dict.get("foo"), 200) self.assertEquals(self.param_dict.get("bar"), 200) # Now let's only have it update 1 parameter using a list sample_input = "foo=300, bar=400" self.assertTrue(self.param_dict.update(sample_input, target_params=["foo"])) self.assertEquals(self.param_dict.get("foo"), 300) self.assertEquals(self.param_dict.get("bar"), 200) # Test our exceptions with self.assertRaises(KeyError): self.param_dict.update(sample_input, "key_does_not_exist") with self.assertRaises(InstrumentParameterException): self.param_dict.update(sample_input, {'bad': "key_does_not_exist"}) def test_visibility_list(self): lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_WRITE) lst.sort() self.assertEquals(lst, ["bar", "foo"]) lst = self.param_dict.get_visibility_list(ParameterDictVisibility.DIRECT_ACCESS) lst.sort() self.assertEquals(lst, ["baz", "qut"]) lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY) lst.sort() self.assertEquals(lst, ["bat", "qux"]) lst = self.param_dict.get_visibility_list(ParameterDictVisibility.IMMUTABLE) lst.sort() self.assertEquals(lst, ["dil", "pho"]) def test_function_values(self): """ Make sure we can add and update values with functions instead of patterns """ self.param_dict.add_parameter( FunctionParameter("fn_foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add_parameter( FunctionParameter("fn_bar", lambda x: bool(x & 2), # bit map example lambda x: str(x), direct_access=True, startup_param=True, value=False, visibility=ParameterDictVisibility.READ_WRITE) ) # check defaults just to be safe val = self.param_dict.get("fn_foo") self.assertEqual(val, 1) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) self.param_dict.update(1005) # just change first in list val = self.param_dict.get("fn_foo") self.assertEqual(val, 3) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # fn_bar does not get updated here result = self.param_dict.update_many(1205) self.assertEqual(result['fn_foo'], True) self.assertEqual(len(result), 1) val = self.param_dict.get("fn_foo") self.assertEqual(val, 4) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # both are updated now result = self.param_dict.update_many(6) self.assertEqual(result['fn_foo'], True) self.assertEqual(result['fn_bar'], True) self.assertEqual(len(result), 2) val = self.param_dict.get("fn_foo") self.assertEqual(val, 0) val = self.param_dict.get("fn_bar") self.assertEqual(val, True) def test_mixed_pdv_types(self): """ Verify we can add different types of PDVs in one container """ self.param_dict.add_parameter( FunctionParameter("fn_foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add_parameter( RegexParameter("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, value=10, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, value=15, visibility=ParameterDictVisibility.READ_WRITE) self.assertEqual(self.param_dict.get("fn_foo"), 1) self.assertEqual(self.param_dict.get("foo"), 10) self.assertEqual(self.param_dict.get("bar"), 15) def test_base_update(self): pdv = Parameter("foo", lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) result = pdv.update(1) self.assertEqual(result, True) self.assertEqual(pdv.get_value(), 1) # Its a base class...monkey see, monkey do result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.get_value(), "foo=1") def test_regex_val(self): pdv = RegexParameter("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) result = pdv.update(1) self.assertEqual(result, False) self.assertEqual(pdv.get_value(), 12) result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.get_value(), 1) def test_function_val(self): pdv = FunctionParameter("foo", self.pick_byte2, lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) self.assertRaises(TypeError, pdv.update(1)) result = pdv.update("1205") self.assertEqual(pdv.get_value(), 4) self.assertEqual(result, True) def test_set_init_value(self): result = self.param_dict.get("foo") self.assertEqual(result, None) self.param_dict.set_init_value("foo", 42) result = self.param_dict.get_init_value("foo") self.assertEqual(result, 42) def test_schema_generation(self): self.maxDiff = None result = self.param_dict.generate_dict() json_result = json.dumps(result, indent=4, sort_keys=True) log.debug("Expected: %s", self.target_schema) log.debug("Result: %s", json_result) self.assertEqual(result, self.target_schema) def test_empty_schema(self): self.param_dict = ProtocolParameterDict() result = self.param_dict.generate_dict() self.assertEqual(result, {}) def test_bad_descriptions(self): self.param_dict._param_dict["foo"].description = None self.param_dict._param_dict["foo"].value = None self.assertRaises(InstrumentParameterException, self.param_dict.get_init_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_default_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.set_default, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_init_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_menu_path_read, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_submenu_read, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_menu_path_write, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_submenu_write, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.format, "foo", 1) self.assertRaises(InstrumentParameterException, self.param_dict.get_direct_access_list) self.assertRaises(InstrumentParameterException, self.param_dict.is_startup_param, "foo") def test_set(self): """ Test a simple set of the parameter. Make sure the right values get called and the correct exceptions are raised. """ new_param = FunctionParameter("foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1000, visibility=ParameterDictVisibility.READ_WRITE) self.assertEquals(new_param.get_value(), 1000) self.assertEquals(self.param_dict.get("foo"), None) # overwrites existing param self.param_dict.add_parameter(new_param) self.assertEquals(self.param_dict.get("foo"), 1000) self.param_dict.set_value("foo", 2000) self.assertEquals(self.param_dict.get("foo"), 2000) def test_invalid_type(self): self.assertRaises(InstrumentParameterException, FunctionParameter, "fn_bar", lambda x: bool(x & 2), # bit map example lambda x: str(x), direct_access=True, startup_param=True, value=False, type="bad_type", visibility=ParameterDictVisibility.READ_WRITE) def test_get(self): """ test getting values with expiration """ # from mi.core.exceptions import InstrumentParameterExpirationException pd = ProtocolParameterDict() # No expiration, should work just fine pd.add('noexp', r'', None, None, expiration=None) pd.add('zeroexp', r'', None, None, expiration=0) pd.add('lateexp', r'', None, None, expiration=2) ### # Set and get with no expire ### pd.set_value('noexp', 1) self.assertEqual(pd.get('noexp'), 1) ### # Set and get with a 0 expire ### basetime = pd.get_current_timestamp() pd.set_value('zeroexp', 2) # We should fail because we are calculating exp against current time with self.assertRaises(InstrumentParameterExpirationException): pd.get('zeroexp') # Should succeed because exp is calculated using basetime self.assertEqual(pd.get('zeroexp', basetime), 2) ### # Set and get with a delayed expire ### basetime = pd.get_current_timestamp() futuretime = pd.get_current_timestamp(3) self.assertGreater(futuretime - basetime, 3) pd.set_value('lateexp', 2) # Success because data is not expired self.assertEqual(pd.get('lateexp', basetime), 2) # Fail because data is expired (simulated three seconds from now) with self.assertRaises(InstrumentParameterExpirationException): pd.get('lateexp', futuretime) def test_regex_flags(self): pdv = RegexParameter("foo", r'.+foo=(\d+).+', lambda match: int(match.group(1)), lambda x: str(x), regex_flags=re.DOTALL, value=12) # Assert something good with dotall update() self.assertTrue(pdv) pdv.update("\n\nfoo=1212\n\n") self.assertEqual(pdv.get_value(), 1212) # negative test with no regex_flags pdv = RegexParameter("foo", r'.+foo=(\d+).+', lambda match: int(match.group(1)), lambda x: str(x), value=12) # Assert something good with dotall update() self.assertTrue(pdv) pdv.update("\n\nfoo=1212\n\n") self.assertEqual(pdv.get_value(), 12) self.assertRaises(TypeError, RegexParameter, "foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), regex_flags="bad flag", value=12) def test_format_current(self): self.param_dict.add("test_format", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: x + 5, value=10) self.assertEqual(self.param_dict.format("test_format", 20), 25) self.assertEqual(self.param_dict.format("test_format"), 15) self.assertRaises(KeyError, self.param_dict.format, "bad_name") def _assert_metadata_change(self): new_dict = self.param_dict.generate_dict() log.debug("Generated dictionary: %s", new_dict) self.assertEqual(new_dict["qut"][ParameterDictKey.DESCRIPTION], "QutFileDesc") self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutDisplay") self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.UNITS], "QutFileUnits") self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.DESCRIPTION], "QutFileValueDesc") self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.TYPE], "QutFileType") # Should come from hard code # self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutFileName") # from base hard code new_dict = self.param_dict.generate_dict() self.assertEqual(new_dict["baz"][ParameterDictKey.DESCRIPTION], "The baz parameter") self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.UNITS], "nano-bazers") self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.DESCRIPTION], "Should be an integer between 2 and 2000") self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.TYPE], ParameterDictType.INT) self.assertEqual(new_dict["baz"][ParameterDictKey.DISPLAY_NAME], "Baz") self.assertTrue('extra_param' not in new_dict)
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit) self._protocol_fsm.add_handler( ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct) # Add build handlers for device commands. self._add_build_handler(Command.BATTERY, self._build_simple_command) # Add response handlers for device commands. self._add_response_handler(Command.BATTERY, self._parse_battery_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(RASFL_SampleDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) """ if return_list != []: log.debug("sieve_function: raw_data=%s, return_list=%s" %(raw_data, return_list)) """ return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s" % chunk) self._extract_sample(RASFL_SampleDataParticle, RASFL_SampleDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # implement virtual methods from base class. ######################################################################## def apply_startup_params(self): """ Apply sample_interval startup parameter. """ config = self.get_startup_config() log.debug("apply_startup_params: startup config = %s" % config) if config.has_key(Parameter.SAMPLE_INTERVAL): log.debug("apply_startup_params: setting sample_interval to %d" % config[Parameter.SAMPLE_INTERVAL]) self._param_dict.set_value(Parameter.SAMPLE_INTERVAL, config[Parameter.SAMPLE_INTERVAL]) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ # force to command mode, this instrument has no autosample mode next_state = ProtocolState.COMMAND result = ResourceAgentState.COMMAND log.debug("_handler_unknown_discover: state = %s", next_state) return (next_state, result) ######################################################################## # Command handlers. # just implemented to make DA possible, instrument has no actual command mode ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ # Command device to update parameters and send a config change event if needed. self._update_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ next_state = None result = None return (next_state, result) def _handler_command_start_direct(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.DIRECT_ACCESS next_agent_state = ResourceAgentState.DIRECT_ACCESS return (next_state, (next_agent_state, result)) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ pass def _handler_direct_access_execute_direct(self, data): next_state = None result = None self._do_cmd_direct(data) return (next_state, result) def _handler_direct_access_stop_direct(self): result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND return (next_state, (next_agent_state, result)) ######################################################################## # general handlers. ######################################################################## def _handler_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None time_format = "%Y/%m/%d %H:%M:%S" str_val = get_timestamp_delayed(time_format) log.debug("Setting instrument clock to '%s'", str_val) self._do_cmd_resp(Command.STOP, expected_prompt=Prompt.STOPPED) try: self._do_cmd_resp(Command.SET_CLOCK, str_val, expected_prompt=Prompt.CR_NL) finally: # ensure that we try to start the instrument sampling again self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO) return (next_state, (next_agent_state, result)) ######################################################################## # Private helpers. ######################################################################## def _wakeup(self, wakeup_timeout=10, response_timeout=3): """ Over-ridden because waking this instrument up is a multi-step process with two different requests required @param wakeup_timeout The timeout to wake the device. @param response_timeout The time to look for response to a wakeup attempt. @throw InstrumentTimeoutException if the device could not be woken. """ sleep_time = .1 command = Command.END_OF_LINE # Grab start time for overall wakeup timeout. starttime = time.time() while True: # Clear the prompt buffer. log.debug("_wakeup: clearing promptbuf: %s" % self._promptbuf) self._promptbuf = '' # Send a command and wait delay amount for response. log.debug('_wakeup: Sending command %s, delay=%s' % (command.encode("hex"), response_timeout)) for char in command: self._connection.send(char) time.sleep(INTER_CHARACTER_DELAY) sleep_amount = 0 while True: time.sleep(sleep_time) if self._promptbuf.find(Prompt.COMMAND_INPUT) != -1: # instrument is awake log.debug('_wakeup: got command input prompt %s' % Prompt.COMMAND_INPUT) # add inter-character delay which _do_cmd_resp() incorrectly doesn't add to the start of a transmission time.sleep(INTER_CHARACTER_DELAY) return Prompt.COMMAND_INPUT if self._promptbuf.find(Prompt.ENTER_CTRL_C) != -1: command = Command.CONTROL_C break if self._promptbuf.find(Prompt.PERIOD) == 0: command = Command.CONTROL_C break sleep_amount += sleep_time if sleep_amount >= response_timeout: log.debug( "_wakeup: expected response not received, buffer=%s" % self._promptbuf) break if time.time() > starttime + wakeup_timeout: raise InstrumentTimeoutException( "_wakeup(): instrument failed to wakeup in %d seconds time" % wakeup_timeout) def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="synchronize clock") def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.BATTERY, r'Battery: (.*)V', lambda match: match.group(1), lambda string: str(string), type=ParameterDictType.STRING, display_name="battery", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add( Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE) def _do_cmd_resp(self, cmd, *args, **kwargs): CommandResponseInstrumentProtocol._do_cmd_resp( self, cmd, args, kwargs, write_delay=INTER_CHARACTER_DELAY) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ log.debug("_update_params:") self._do_cmd_resp(Command.BATTERY) def _parse_battery_response(self, response, prompt): """ Parse handler for battery command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if clock command misunderstood. """ log.debug("_parse_battery_response: response=%s, prompt=%s" % (response, prompt)) if prompt == Prompt.UNRECOGNIZED_COMMAND: raise InstrumentProtocolException( 'battery command not recognized: %s.' % response) if not self._param_dict.update(response): raise InstrumentProtocolException( 'battery command not parsed: %s.' % response) return
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ __metaclass__ = META_LOGGER def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. handlers = { ProtocolState.UNKNOWN: [ (ProtocolEvent.ENTER, self._handler_generic_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.DISCOVER, self._handler_unknown_discover), ], ProtocolState.AUTOSAMPLE: [ (ProtocolEvent.ENTER, self._handler_autosample_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.GET, self._handler_command_get), (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample), (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command), ], ProtocolState.COMMAND: [ (ProtocolEvent.ENTER, self._handler_command_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.GET, self._handler_command_get), (ProtocolEvent.SET, self._handler_command_set), (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample), (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command), ], } for state in handlers: for event, handler in handlers[state]: self._protocol_fsm.add_handler(state, event, handler) # Construct the metadata dictionaries self._build_command_dict() self._build_driver_dict() # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # set up scheduled event handling self.initialize_scheduler() self._schedulers = [] def _generate_particle(self, stream_name, count=1): # we're faking it anyway, send these as fast as we can... # the overall rate will be close enough particle = VirtualParticle(stream_name, port_timestamp=0) for x in range(count): particle.contents["port_timestamp"] = ntplib.system_to_ntp_time(time.time()) self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate()) time.sleep(0.001) def _create_scheduler(self, stream_name, rate): job_name = stream_name if rate > 1: interval = 1 else: interval = 1 / rate config = { DriverConfigKey.SCHEDULER: { job_name: { DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: interval, } } } } self.set_init_params(config) self._schedulers.append(stream_name) if rate > 1: self._add_scheduler(stream_name, functools.partial(self._generate_particle, stream_name, count=rate)) else: self._add_scheduler(stream_name, functools.partial(self._generate_particle, stream_name)) def _delete_all_schedulers(self): for name in self._schedulers: try: self._remove_scheduler(name) except: pass def _got_chunk(self, chunk, ts): """ Process chunk output by the chunker. Generate samples and (possibly) react @param chunk: data @param ts: ntp timestamp @return sample @throws InstrumentProtocolException """ return def _filter_capabilities(self, events): """ Filter a list of events to only include valid capabilities @param events: list of events to be filtered @return: list of filtered events """ return [x for x in events if Capability.has(x)] def _build_command_dict(self): """ Populate the command dictionary with commands. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample") def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _update_params(self, *args, **kwargs): """ Update the param dictionary based on instrument response """ def _set_params(self, *args, **kwargs): if len(args) < 1: raise InstrumentParameterException("Set command requires a parameter dict.") params = args[0] if not isinstance(params, dict): raise InstrumentParameterException("Set parameters not a dict.") self._param_dict = ProtocolParameterDict() for param in params: log.info("Creating new parameter: %s", param) self._param_dict.add(param, "", None, None) self._param_dict.set_value(param, params[param]) def set_init_params(self, config): if not isinstance(config, dict): raise InstrumentParameterException("Invalid init config format") self._startup_config = config param_config = config.get(DriverConfigKey.PARAMETERS) if param_config: for name in param_config.keys(): self._param_dict.add(name, "", None, None) log.debug("Setting init value for %s to %s", name, param_config[name]) self._param_dict.set_init_value(name, param_config[name]) def _very_long_command(self, *args, **kwargs): return None, time.sleep(30) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_discover(self, *args, **kwargs): """ Process discover event """ next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result) ######################################################################## # Autosample handlers. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Enter autosample state. """ self._init_params() for stream_name in self._param_dict.get_keys(): self._create_scheduler(stream_name, self._param_dict.get(stream_name)) self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_autosample_stop_autosample(self, *args, **kwargs): """ Stop autosample @return next_state, (next_state, result) """ next_state = ProtocolState.COMMAND result = [] self._delete_all_schedulers() return next_state, (next_state, result) ######################################################################## # Command handlers. ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ self._init_params() self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_get(self, *args, **kwargs): """ Process GET event """ return self._handler_get(*args, **kwargs) def _handler_command_set(self, *args, **kwargs): """ Perform a set command. @param args[0] parameter : value dict. @throws InstrumentParameterException """ next_state = None result = [] self._set_params(*args, **kwargs) return next_state, (next_state, result) def _handler_command_start_autosample(self): """ Start autosample @return next_state, (next_state, result) """ next_state = ProtocolState.AUTOSAMPLE result = [] return next_state, (next_state, result) ######################################################################## # Generic handlers. ######################################################################## def _handler_generic_enter(self, *args, **kwargs): """ Generic enter state handler """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_generic_exit(self, *args, **kwargs): """
class Protocol(SamiProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. SamiProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. ### # most of these are defined in the base class with exception of handlers # defined below that differ for the two instruments (what defines # success and the timeout duration) ### # this state would be entered whenever an ACQUIRE_SAMPLE event occurred # while in the AUTOSAMPLE state and will last anywhere from a few # seconds to ~12 minutes depending on instrument and the type of # sampling. self._protocol_fsm.add_handler(ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler(ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) # this state would be entered whenever an ACQUIRE_SAMPLE event occurred # while in either the COMMAND state (or via the discover transition # from the UNKNOWN state with the instrument unresponsive) and will # last anywhere from a few seconds to 3 minutes depending on instrument # and sample type. self._protocol_fsm.add_handler(ProtocolState.POLLED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler(ProtocolState.POLLED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) # Add build handlers for device commands. ### primarily defined in base class self._add_build_handler(InstrumentCommand.ACQUIRE_SAMPLE_DEV1, self._build_sample_dev1) # Add response handlers for device commands. ### primarily defined in base class self._add_response_handler(InstrumentCommand.ACQUIRE_SAMPLE_DEV1, self._build_response_sample_dev1) # Add sample handlers # State state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # build the chunker self._chunker = StringChunker(Protocol.sieve_function) @staticmethod def sieve_function(raw_data): """ The method that splits samples """ return_list = [] sieve_matchers = [REGULAR_STATUS_REGEX_MATCHER, CONTROL_RECORD_REGEX_MATCHER, SAMI_SAMPLE_REGEX_MATCHER, DEV1_SAMPLE_REGEX_MATCHER, CONFIGURATION_REGEX_MATCHER] for matcher in sieve_matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ self._extract_sample(SamiRegularStatusDataParticle, REGULAR_STATUS_REGEX_MATCHER, chunk, timestamp) self._extract_sample(SamiControlRecordDataParticle, CONTROL_RECORD_REGEX_MATCHER, chunk, timestamp) self._extract_sample(Pco2wSamiSampleDataParticle, SAMI_SAMPLE_REGEX_MATCHER, chunk, timestamp) self._extract_sample(Pco2wDev1SampleDataParticle, DEV1_SAMPLE_REGEX_MATCHER, chunk, timestamp) self._extract_sample(Pco2wConfigurationDataParticle, CONFIGURATION_REGEX_MATCHER, chunk, timestamp) ######################################################################## # Build Command, Driver and Parameter dictionaries ######################################################################## def _build_param_dict(self): """ For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() ### example configuration string # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \ # '000000000D000000000D000000000D07' + \ # '1020FF54181C01003814' + \ # '000000000000000000000000000000000000000000000000000' + \ # '000000000000000000000000000000000000000000000000000' + \ # '0000000000000000000000000000' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE # ### self._param_dict.add(Parameter.LAUNCH_TIME, CONFIGURATION_REGEX, lambda match: int(match.group(1), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='launch time') self._param_dict.add(Parameter.START_TIME_FROM_LAUNCH, CONFIGURATION_REGEX, lambda match: int(match.group(2), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02C7EA00, visibility=ParameterDictVisibility.READ_ONLY, display_name='start time after launch time') self._param_dict.add(Parameter.STOP_TIME_FROM_START, CONFIGURATION_REGEX, lambda match: int(match.group(3), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01E13380, visibility=ParameterDictVisibility.READ_ONLY, display_name='stop time after start time') self._param_dict.add(Parameter.MODE_BITS, CONFIGURATION_REGEX, lambda match: int(match.group(4), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0A, visibility=ParameterDictVisibility.READ_ONLY, display_name='mode bits (set to 00001010)') self._param_dict.add(Parameter.SAMI_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(5), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami sample interval') self._param_dict.add(Parameter.SAMI_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(6), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x04, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami driver version') self._param_dict.add(Parameter.SAMI_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(7), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami parameter pointer') self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(8), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 sample interval') self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(9), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 driver version') self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(10), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0B, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 parameter pointer') self._param_dict.add(Parameter.DEVICE2_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(11), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 sample interval') self._param_dict.add(Parameter.DEVICE2_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(12), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 driver version') self._param_dict.add(Parameter.DEVICE2_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(13), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 parameter pointer') self._param_dict.add(Parameter.DEVICE3_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(14), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 sample interval') self._param_dict.add(Parameter.DEVICE3_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(15), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 driver version') self._param_dict.add(Parameter.DEVICE3_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(16), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 parameter pointer') self._param_dict.add(Parameter.PRESTART_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(17), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart sample interval') self._param_dict.add(Parameter.PRESTART_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(18), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart driver version') self._param_dict.add(Parameter.PRESTART_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(19), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart parameter pointer') self._param_dict.add(Parameter.GLOBAL_CONFIGURATION, CONFIGURATION_REGEX, lambda match: int(match.group(20), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='global bits (set to 00000111)') self._param_dict.add(Parameter.PUMP_PULSE, CONFIGURATION_REGEX, lambda match: int(match.group(21), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x10, visibility=ParameterDictVisibility.READ_ONLY, display_name='pump pulse duration') self._param_dict.add(Parameter.PUMP_DURATION, CONFIGURATION_REGEX, lambda match: int(match.group(22), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x20, visibility=ParameterDictVisibility.READ_ONLY, display_name='pump measurement duration') self._param_dict.add(Parameter.SAMPLES_PER_MEASUREMENT, CONFIGURATION_REGEX, lambda match: int(match.group(23), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0xFF, visibility=ParameterDictVisibility.READ_ONLY, display_name='samples per measurement') self._param_dict.add(Parameter.CYCLES_BETWEEN_BLANKS, CONFIGURATION_REGEX, lambda match: int(match.group(24), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0xA8, visibility=ParameterDictVisibility.READ_ONLY, display_name='cycles between blanks') self._param_dict.add(Parameter.NUMBER_REAGENT_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(25), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x18, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of reagent cycles') self._param_dict.add(Parameter.NUMBER_BLANK_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(26), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x1C, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of blank cycles') self._param_dict.add(Parameter.FLUSH_PUMP_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(27), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='flush pump interval') self._param_dict.add(Parameter.BIT_SWITCHES, CONFIGURATION_REGEX, lambda match: int(match.group(28), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='bit switches') self._param_dict.add(Parameter.NUMBER_EXTRA_PUMP_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(29), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x38, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of extra pump cycles') self._param_dict.add(Parameter.EXTERNAL_PUMP_SETTINGS, CONFIGURATION_REGEX, lambda match: int(match.group(30), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x14, visibility=ParameterDictVisibility.READ_ONLY, display_name='external pump settings') ######################################################################### ## General (for POLLED and SCHEDULED states) Sample handlers. ######################################################################### def _handler_sample_success(self, *args, **kwargs): next_state = None result = None return (next_state, result) def _handler_sample_timeout(self, ): next_state = None result = None return (next_state, result) ######################################################################## # Command handlers. ######################################################################## def _build_sample_dev1(self): pass ######################################################################## # Response handlers. ######################################################################## def _build_response_sample_dev1(self): pass
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ __metaclass__ = META_LOGGER def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. handlers = { ProtocolState.UNKNOWN: [ (ProtocolEvent.ENTER, self._handler_generic_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.DISCOVER, self._handler_unknown_discover), ], ProtocolState.AUTOSAMPLE: [ (ProtocolEvent.ENTER, self._handler_autosample_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.GET, self._handler_command_get), (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample), (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command), ], ProtocolState.COMMAND: [ (ProtocolEvent.ENTER, self._handler_command_enter), (ProtocolEvent.EXIT, self._handler_generic_exit), (ProtocolEvent.GET, self._handler_command_get), (ProtocolEvent.SET, self._handler_command_set), (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample), (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command), ], } for state in handlers: for event, handler in handlers[state]: self._protocol_fsm.add_handler(state, event, handler) # Construct the metadata dictionaries self._build_command_dict() self._build_driver_dict() # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # set up scheduled event handling self.initialize_scheduler() self._schedulers = [] def _generate_particle(self, stream_name, count=1): # we're faking it anyway, send these as fast as we can... # the overall rate will be close enough particle = VirtualParticle(stream_name, port_timestamp=0) for x in range(count): particle.contents['port_timestamp'] = ntplib.system_to_ntp_time( time.time()) self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate()) time.sleep(.001) def _create_scheduler(self, stream_name, rate): job_name = stream_name if rate > 1: interval = 1 else: interval = 1 / rate config = { DriverConfigKey.SCHEDULER: { job_name: { DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: interval } } } } self.set_init_params(config) self._schedulers.append(stream_name) if rate > 1: self._add_scheduler( stream_name, functools.partial(self._generate_particle, stream_name, count=rate)) else: self._add_scheduler( stream_name, functools.partial(self._generate_particle, stream_name)) def _delete_all_schedulers(self): for name in self._schedulers: try: self._remove_scheduler(name) except: pass def _got_chunk(self, chunk, ts): """ Process chunk output by the chunker. Generate samples and (possibly) react @param chunk: data @param ts: ntp timestamp @return sample @throws InstrumentProtocolException """ return def _filter_capabilities(self, events): """ Filter a list of events to only include valid capabilities @param events: list of events to be filtered @return: list of filtered events """ return [x for x in events if Capability.has(x)] def _build_command_dict(self): """ Populate the command dictionary with commands. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample") def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _update_params(self, *args, **kwargs): """ Update the param dictionary based on instrument response """ def _set_params(self, *args, **kwargs): if len(args) < 1: raise InstrumentParameterException( 'Set command requires a parameter dict.') params = args[0] if not isinstance(params, dict): raise InstrumentParameterException('Set parameters not a dict.') self._param_dict = ProtocolParameterDict() for param in params: log.info('Creating new parameter: %s', param) self._param_dict.add(param, '', None, None) self._param_dict.set_value(param, params[param]) def set_init_params(self, config): if not isinstance(config, dict): raise InstrumentParameterException("Invalid init config format") self._startup_config = config param_config = config.get(DriverConfigKey.PARAMETERS) if param_config: for name in param_config.keys(): self._param_dict.add(name, '', None, None) log.debug("Setting init value for %s to %s", name, param_config[name]) self._param_dict.set_init_value(name, param_config[name]) def _very_long_command(self, *args, **kwargs): return None, time.sleep(30) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_discover(self, *args, **kwargs): """ Process discover event """ next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result) ######################################################################## # Autosample handlers. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Enter autosample state. """ self._init_params() for stream_name in self._param_dict.get_keys(): self._create_scheduler(stream_name, self._param_dict.get(stream_name)) self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_autosample_stop_autosample(self, *args, **kwargs): """ Stop autosample @return next_state, (next_state, result) """ next_state = ProtocolState.COMMAND result = [] self._delete_all_schedulers() return next_state, (next_state, result) ######################################################################## # Command handlers. ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ self._init_params() self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_get(self, *args, **kwargs): """ Process GET event """ return self._handler_get(*args, **kwargs) def _handler_command_set(self, *args, **kwargs): """ Perform a set command. @param args[0] parameter : value dict. @throws InstrumentParameterException """ next_state = None result = [] self._set_params(*args, **kwargs) return next_state, (next_state, result) def _handler_command_start_autosample(self): """ Start autosample @return next_state, (next_state, result) """ next_state = ProtocolState.AUTOSAMPLE result = [] return next_state, (next_state, result) ######################################################################## # Generic handlers. ######################################################################## def _handler_generic_enter(self, *args, **kwargs): """ Generic enter state handler """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_generic_exit(self, *args, **kwargs): """
class DataSetDriver(object): """ Base class for data set drivers. Provides: - an interface via callback to publish data - an interface via callback to persist driver state - an interface via callback to handle exceptions - an start and stop sampling - a client interface for execute resource Subclasses need to include harvesters and parsers and be specialized to handle the interaction between the two. Configurations should contain keys from the DataSetDriverConfigKey class and should look something like this example (more full documentation in the "Dataset Agent Architecture" page on the OOI wiki): { 'harvester': { 'directory': '/tmp/dsatest', 'storage_directory': '/tmp/stored_dsatest', 'pattern': '*.txt', 'frequency': 1, 'file_mod_wait_time': 30, }, 'parser': {} 'driver': { 'records_per_second' 'harvester_polling_interval' 'batched_particle_count' } } """ def __init__(self, config, memento, data_callback, state_callback, event_callback, exception_callback): self._config = copy.deepcopy(config) self._data_callback = data_callback self._state_callback = state_callback self._event_callback = event_callback self._exception_callback = exception_callback self._memento = memento self._publisher_thread = None self._verify_config() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._resource_id = None self._param_dict = ProtocolParameterDict() self._cmd_dict = ProtocolCommandDict() self._driver_dict = DriverDict() self._build_command_dict() self._build_driver_dict() self._build_param_dict() def shutdown(self): self.stop_sampling() def start_sampling(self): """ Start a new thread to monitor for data """ self._start_sampling() self._start_publisher_thread() def stop_sampling(self): """ Stop the sampling thread """ log.debug("Stopping sampling and publisher now") self._stop_sampling() self._stop_publisher_thread() def _start_sampling(self): raise NotImplementedException('virtual method needs to be specialized') def _stop_sampling(self): raise NotImplementedException('virtual method needs to be specialized') def _is_sampling(self): """ Currently the drivers only have two states, command and streaming and all resource commands are common, either start or stop autosample. Therefore we didn't implement an enitre state machine to manage states and commands. If it does get more complex than this we should take the time to implement a state machine to add some flexibility """ raise NotImplementedException('virtual method needs to be specialized') def cmd_dvr(self, cmd, *args, **kwargs): log.warn("DRIVER: cmd_dvr %s", cmd) if cmd == 'execute_resource': resource_cmd = args[0] if resource_cmd == DriverEvent.START_AUTOSAMPLE: return (ResourceAgentState.STREAMING, None) elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE: self.stop_sampling() return (ResourceAgentState.COMMAND, None) else: log.error("Unhandled resource command: %s", resource_cmd) raise elif cmd == 'get_resource_capabilities': return self.get_resource_capabilities() elif cmd == 'set_resource': return self.set_resource(*args, **kwargs) elif cmd == 'get_resource': return self.get_resource(*args, **kwargs) elif cmd == 'get_config_metadata': return self.get_config_metadata(*args, **kwargs) elif cmd == 'disconnect': pass elif cmd == 'initialize': pass else: log.error("Unhandled command: %s", cmd) raise InstrumentStateException("Unhandled command: %s" % cmd) def get_resource_capabilities(self, current_state=True, *args, **kwargs): """ Return driver commands and parameters. @param current_state True to retrieve commands available in current state, otherwise reutrn all commands. @retval list of AgentCapability objects representing the drivers capabilities. @raises NotImplementedException if not implemented by subclass. """ res_params = self._param_dict.get_keys() res_cmds = [DriverEvent.STOP_AUTOSAMPLE, DriverEvent.START_AUTOSAMPLE] if current_state and self._is_sampling(): res_cmds = [DriverEvent.STOP_AUTOSAMPLE] elif current_state and not self._is_sampling(): res_cmds = [DriverEvent.START_AUTOSAMPLE] return [res_cmds, res_params] def set_resource(self, *args, **kwargs): """ Set the driver parameter """ log.trace("start set_resource") try: params = args[0] except IndexError: raise InstrumentParameterException('Set command requires a parameter dict.') log.trace("set_resource: iterate through params: %s", params) for (key, val) in params.iteritems(): if key in [DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND]: if not isinstance(val, int): raise InstrumentParameterException("%s must be an integer" % key) if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]: if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key) if val <= 0: raise InstrumentParameterException("%s must be > 0" % key) self._param_dict.set_value(key, val) # Set the driver parameters self._generate_particle_count = self._param_dict.get(DriverParameter.BATCHED_PARTICLE_COUNT) self._particle_count_per_second = self._param_dict.get(DriverParameter.RECORDS_PER_SECOND) self._polling_interval = self._param_dict.get(DriverParameter.PUBLISHER_POLLING_INTERVAL) log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second, self._generate_particle_count) def get_resource(self, *args, **kwargs): """ Get driver parameter """ result = {} try: params = args[0] except IndexError: raise InstrumentParameterException('Set command requires a parameter list.') # If all params requested, retrieve config. if params == DriverParameter.ALL: result = self._param_dict.get_config() # If not all params, confirm a list or tuple of params to retrieve. # Raise if not a list or tuple. # Retrieve each key in the list, raise if any are invalid. else: if not isinstance(params, (list, tuple)): raise InstrumentParameterException('Get argument not a list or tuple.') result = {} for key in params: try: val = self._param_dict.get(key) result[key] = val except KeyError: raise InstrumentParameterException(('%s is not a valid parameter.' % key)) return result def get_config_metadata(self): """ Return the configuration metadata object in JSON format @retval The description of the parameters, commands, and driver info in a JSON string @see https://confluence.oceanobservatories.org/display/syseng/CIAD+MI+SV+Instrument+Driver-Agent+parameter+and+command+metadata+exchange """ log.debug("Getting metadata from driver...") log.debug("Getting metadata dict from protocol...") return_dict = {} return_dict[ConfigMetadataKey.DRIVER] = self._driver_dict.generate_dict() return_dict[ConfigMetadataKey.COMMANDS] = self._cmd_dict.generate_dict() return_dict[ConfigMetadataKey.PARAMETERS] = self._param_dict.generate_dict() return return_dict def _verify_config(self): """ virtual method to verify the supplied driver configuration is value. Must be overloaded in sub classes. raises an ConfigurationException when a configuration error is detected. """ raise NotImplementedException('virtual methond needs to be specialized') def _build_driver_dict(self): """ Populate the driver dictionary with options """ pass def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(DriverEvent.START_AUTOSAMPLE, display_name="start autosample") self._cmd_dict.add(DriverEvent.STOP_AUTOSAMPLE, display_name="stop autosample") def _build_param_dict(self): """ Setup three common driver parameters """ self._param_dict.add_parameter( Parameter( DriverParameter.RECORDS_PER_SECOND, int, value=60, type=ParameterDictType.INT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Records Per Second", description="Number of records to process per second") ) self._param_dict.add_parameter( Parameter( DriverParameter.PUBLISHER_POLLING_INTERVAL, float, value=1, type=ParameterDictType.FLOAT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Harvester Polling Interval", description="Duration in minutes to wait before checking for new files.") ) self._param_dict.add_parameter( Parameter( DriverParameter.BATCHED_PARTICLE_COUNT, int, value=1, type=ParameterDictType.INT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Batched Particle Count", description="Number of particles to batch before sending to the agent") ) config = self._config.get(DataSourceConfigKey.DRIVER, {}) log.debug("set_resource on startup with: %s", config) self.set_resource(config) def _start_publisher_thread(self): self._publisher_thread = gevent.spawn(self._publisher_loop) self._publisher_shutdown = False def _stop_publisher_thread(self): log.debug("Signal shutdown") self._publisher_shutdown = True if self._publisher_thread: self._publisher_thread.kill(block=False) log.debug("shutdown complete") def _publisher_loop(self): """ Main loop to listen for new files to parse. Parse them and move on. """ log.info("Starting main publishing loop") try: while(not self._publisher_shutdown): self._poll() gevent.sleep(self._polling_interval) except Exception as e: log.error("Exception in publisher thread (resource id: %s): %s", self._resource_id, traceback.format_exc(e)) self._exception_callback(e) log.debug("publisher thread detected shutdown request") def _poll(self): raise NotImplementedException('virtual methond needs to be specialized') def _new_file_exception(self): raise NotImplementedException('virtual methond needs to be specialized') def _sample_exception_callback(self, exception): """ Publish an event when a sample exception is detected """ self._event_callback(event_type="ResourceAgentErrorEvent", error_msg = "%s" % exception) def _raise_new_file_event(self, name): """ Raise a ResourceAgentIOEvent when a new file is detected. Add file stats to the payload of the event. """ s = os.stat(name) checksum = "" with open(name, 'rb') as filehandle: checksum = hashlib.md5(filehandle.read()).hexdigest() stats = { 'name': name, 'size': s.st_size, 'mod': s.st_mtime, 'md5_checksum': checksum } self._event_callback(event_type="ResourceAgentIOEvent", source_type="new file", stats=stats)
class Protocol(MenuInstrumentProtocol): """ Instrument protocol class Subclasses MenuInstrumentProtocol """ def __init__(self, menu, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. MenuInstrumentProtocol.__init__(self, menu, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = InstrumentFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.DISCOVER, self._handler_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_command_get) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_AUTOSAMPLE, self._handler_command_autosample) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.DISCOVER, self._handler_discover) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() # Add build handlers for device commands. self._add_build_handler(Command.BACK_MENU, self._build_menu_command) self._add_build_handler(Command.BLANK, self._build_solo_command) self._add_build_handler(Command.START_AUTOSAMPLE, self._build_menu_command) self._add_build_handler(Command.CHANGE_PARAM, self._build_menu_command) self._add_build_handler(Command.SHOW_PARAM, self._build_menu_command) self._add_build_handler(Command.SENSOR_POWER, self._build_menu_command) self._add_build_handler(Command.DIRECT_SET, self._build_direct_command) self._add_build_handler(Command.CHANGE_CYCLE_TIME, self._build_menu_command) self._add_build_handler(Command.CHANGE_VERBOSE, self._build_menu_command) self._add_build_handler(Command.CHANGE_METADATA_RESTART, self._build_menu_command) self._add_build_handler(Command.CHANGE_METADATA_POWERUP, self._build_menu_command) self._add_build_handler(Command.CHANGE_RES_SENSOR_POWER, self._build_menu_command) self._add_build_handler(Command.CHANGE_INST_AMP_POWER, self._build_menu_command) self._add_build_handler(Command.CHANGE_EH_ISOLATION_AMP_POWER, self._build_menu_command) self._add_build_handler(Command.CHANGE_HYDROGEN_POWER, self._build_menu_command) self._add_build_handler(Command.CHANGE_REFERENCE_TEMP_POWER, self._build_menu_command) # Add response handlers for device commands. #self._add_response_handler(Command.GET, self._parse_get_response) #self._add_response_handler(Command.SET, self._parse_get_response) self._add_response_handler(Command.BACK_MENU, self._parse_menu_change_response) self._add_response_handler(Command.BLANK, self._parse_menu_change_response) self._add_response_handler(Command.SHOW_PARAM, self._parse_show_param_response) self._add_response_handler(Command.CHANGE_CYCLE_TIME, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_VERBOSE, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_METADATA_RESTART, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_METADATA_POWERUP, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_RES_SENSOR_POWER, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_INST_AMP_POWER, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_EH_ISOLATION_AMP_POWER, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_HYDROGEN_POWER, self._parse_menu_change_response) self._add_response_handler(Command.CHANGE_REFERENCE_TEMP_POWER, self._parse_menu_change_response) self._add_response_handler(Command.DIRECT_SET, self._parse_menu_change_response) # Add sample handlers. # State state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # commands sent sent to device to be filtered in responses for telnet DA self._sent_cmds = [] self._chunker = StringChunker(self.sieve_function) @staticmethod def sieve_function(raw_data): """ The method that splits samples """ return_list = [] for match in SAMPLE_REGEX.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _go_to_root_menu(self): """ Get back to the root menu, assuming we are in COMMAND mode. Getting to command mode should be done before this method is called. A discover will get there. """ log.debug("Returning to root menu...") # Issue an enter or two off the bat to get out of any display screens # and confirm command mode try: response = self._do_cmd_resp(Command.BLANK, expected_prompt=Prompt.CMD_PROMPT) while not str(response).lstrip().endswith(Prompt.CMD_PROMPT): response = self._do_cmd_resp(Command.BLANK, expected_prompt=Prompt.CMD_PROMPT) time.sleep(1) except InstrumentTimeoutException: raise InstrumentProtocolException("Not able to get valid command prompt. Is instrument in command mode?") # When you get a --> prompt, do 9's until you get back to the root response = self._do_cmd_resp(Command.BACK_MENU, expected_prompt=MENU_PROMPTS) while not str(response).lstrip().endswith(Prompt.MAIN_MENU): response = self._do_cmd_resp(Command.BACK_MENU, expected_prompt=MENU_PROMPTS) def _filter_capabilities(self, events): """ Define a small filter of the capabilities @param A list of events to consider as capabilities @retval A list of events that are actually capabilities """ events_out = [x for x in events if Capability.has(x)] return events_out def get_resource_capabilities(self, current_state=True): """ """ res_cmds = self._protocol_fsm.get_events(current_state) res_cmds = self._filter_capabilities(res_cmds) res_params = VisibleParameters.list() return [res_cmds, res_params] ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_discover(self, *args, **kwargs): """ Discover current state by going to the root menu @retval (next_state, result) """ next_state = None next_agent_state = None # Try to break in case we are in auto sample self._send_break() next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.IDLE self._go_to_root_menu() return (next_state, next_agent_state) ######################################################################## # Command handlers. ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. @throw InstrumentTimeoutException if the device cannot be woken. @throw InstrumentProtocolException if the update commands and not recognized. """ # Command device to update parameters and send a config change event. # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_get(self, params=None, *args, **kwargs): """ Get parameters while in the command state. @param params List of the parameters to pass to the state @retval returns (next_state, result) where result is a dict {}. No agent state changes happening with Get, so no next_agent_state @throw InstrumentParameterException for invalid parameter """ next_state = None result = None result_vals = {} if (params == None): raise InstrumentParameterException("GET parameter list empty!") if (params == Parameter.ALL): params = [Parameter.CYCLE_TIME, Parameter.EH_ISOLATION_AMP_POWER, Parameter.HYDROGEN_POWER, Parameter.INST_AMP_POWER, Parameter.METADATA_POWERUP, Parameter.METADATA_RESTART, Parameter.REFERENCE_TEMP_POWER, Parameter.RES_SENSOR_POWER, Parameter.VERBOSE] if not isinstance(params, list): raise InstrumentParameterException("GET parameter list not a list!") # Do a bulk update from the instrument since they are all on one page self._update_params() # fill the return values from the update for param in params: if not Parameter.has(param): raise InstrumentParameterException("Invalid parameter!") result_vals[param] = self._param_dict.get(param) result = result_vals log.debug("Get finished, next: %s, result: %s", next_state, result) return (next_state, result) def _handler_command_set(self, params, *args, **kwargs): """Handle setting data from command mode @param params Dict of the parameters and values to pass to the state @retval return (next state, result) @throw InstrumentProtocolException For invalid parameter """ next_state = None result = None result_vals = {} if ((params == None) or (not isinstance(params, dict))): raise InstrumentParameterException() name_values = params for key in name_values.keys(): if not Parameter.has(key): raise InstrumentParameterException() # restrict operations to just the read/write parameters if (key == Parameter.CYCLE_TIME): self._navigate(SubMenu.CYCLE_TIME) (unit, value) = self._from_seconds(name_values[key]) try: self._do_cmd_resp(Command.DIRECT_SET, unit, expected_prompt=[Prompt.CYCLE_TIME_SEC_VALUE_PROMPT, Prompt.CYCLE_TIME_MIN_VALUE_PROMPT]) self._do_cmd_resp(Command.DIRECT_SET, value, expected_prompt=Prompt.CHANGE_PARAM_MENU) except InstrumentProtocolException: self._go_to_root_menu() raise InstrumentProtocolException("Could not set cycle time") # Populate with actual value set result_vals[key] = name_values[key] # re-sync with param dict? self._go_to_root_menu() self._update_params() result = result_vals log.debug("next: %s, result: %s", next_state, result) return (next_state, result) def _handler_command_autosample(self, *args, **kwargs): """ Start autosample mode """ next_state = None next_agent_state = None result = None self._navigate(SubMenu.MAIN) self._do_cmd_no_resp(Command.START_AUTOSAMPLE) next_state = ProtocolState.AUTOSAMPLE next_agent_state = ResourceAgentState.STREAMING return (next_state, (next_agent_state, result)) def _handler_command_start_direct(self): """ """ next_state = None next_agent_state = None result = None next_state = ProtocolState.DIRECT_ACCESS next_agent_state = ResourceAgentState.DIRECT_ACCESS return (next_state, (next_agent_state, result)) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ pass def _handler_direct_access_execute_direct(self, data): """ """ next_state = None result = None self._do_cmd_direct(data) # add sent command to list for 'echo' filtering in callback self._sent_cmds.append(data) return (next_state, result) def _handler_direct_access_stop_direct(self): """ @throw InstrumentProtocolException on invalid command """ next_state = None next_agent_state = None result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND return (next_state, (next_agent_state, result)) ######################################################################## # Autosample handlers ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Enter autosample mode """ self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_autosample_stop(self): """ Stop autosample mode """ next_state = None next_agent_state = None result = None if (self._send_break()): next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND return (next_state, (next_agent_state, result)) ######################################################################## # Command builders ######################################################################## def _build_solo_command(self, cmd): """ Issue a simple command that does NOT require a newline at the end to execute. Likly used for control characters or special characters """ return COMMAND_CHAR[cmd] def _build_menu_command(self, cmd): """ Pick the right character and add a newline """ if COMMAND_CHAR[cmd]: return COMMAND_CHAR[cmd]+self._newline else: raise InstrumentProtocolException("Unknown command character for %s" % cmd) def _build_direct_command(self, cmd, arg): """ Build a command where we just send the argument to the instrument. Ignore the command part, we dont need it here as we are already in a submenu. """ return "%s%s" % (arg, self._newline) ######################################################################## # Command parsers ######################################################################## def _parse_menu_change_response(self, response, prompt): """ Parse a response to a menu change @param response What was sent back from the command that was sent @param prompt The prompt that was returned from the device @retval The prompt that was encountered after the change """ log.trace("Parsing menu change response with prompt: %s", prompt) return prompt def _parse_show_param_response(self, response, prompt): """ Parse the show parameter response screen """ log.trace("Parsing show parameter screen") self._param_dict.update_many(response) ######################################################################## # Utilities ######################################################################## def _wakeup(self, timeout): # Always awake for this instrument! pass def _got_chunk(self, chunk): ''' extract samples from a chunk of data @param chunk: bytes to parse into a sample. ''' self._extract_sample(BarsDataParticle, SAMPLE_REGEX, chunk) def _update_params(self): """Fetch the parameters from the device, and update the param dict. @param args Unused @param kwargs Takes timeout value @throw InstrumentProtocolException @throw InstrumentTimeoutException """ log.debug("Updating parameter dict") old_config = self._param_dict.get_config() self._get_config() new_config = self._param_dict.get_config() if (new_config != old_config): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def _get_config(self, *args, **kwargs): """ Get the entire configuration for the instrument @param params The parameters and values to set Should be a dict of parameters and values @throw InstrumentProtocolException On a deeper issue """ # Just need to show the parameter screen...the parser for the command # does the update_many() self._go_to_root_menu() self._navigate(SubMenu.SHOW_PARAM) self._go_to_root_menu() def _send_break(self, timeout=4): """ Execute an attempts to break out of auto sample (a few if things get garbled). For this instrument, it is done with a ^S, a wait for a \r\n, then another ^S within 1/2 a second @param timeout @retval True if 2 ^S chars were sent with a prompt in the middle, False if not. """ log.debug("Sending break sequence to instrument...") # Timing is an issue, so keep it simple, work directly with the # couple chars instead of command/respose. Could be done that way # though. Just more steps, logic, and delay for such a simple # exchange for count in range(0, 3): self._promptbuf = "" try: self._connection.send("%c" % COMMAND_CHAR[Command.BREAK]) (prompt, result) = self._get_raw_response(timeout, expected_prompt=[Prompt.BREAK_ACK, Prompt.CMD_PROMPT]) if (prompt == Prompt.BREAK_ACK): self._connection.send("%c" % COMMAND_CHAR[Command.BREAK]) (prompt, result) = self._get_response(timeout, expected_prompt=Prompt.CMD_PROMPT) return True elif(prompt == Prompt.CMD_PROMPT): return True except InstrumentTimeoutException: continue log.trace("_send_break failing after several attempts") return False def set_readonly_values(self, *args, **kwargs): """Set read-only values to the instrument. This is usually (only?) done at initialization. @throw InstrumentProtocolException When in the wrong state or something really bad prevents the setting of all values. """ # Let's give it a try in unknown state if (self.get_current_state() != ProtocolState.COMMAND): raise InstrumentProtocolException("Not in command state. Unable to set read-only params") self._go_to_root_menu() self._update_params() for param in self._param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY): if not Parameter.has(param): raise InstrumentParameterException() self._go_to_root_menu() # Only try to change them if they arent set right as it is log.trace("Setting read-only parameter: %s, current paramdict value: %s, init val: %s", param, self._param_dict.get(param), self._param_dict.get_init_value(param)) if (self._param_dict.get(param) != self._param_dict.get_init_value(param)): if (param == Parameter.METADATA_POWERUP): self._navigate(SubMenu.METADATA_POWERUP) result = self._do_cmd_resp(Command.DIRECT_SET, (1+ int(self._param_dict.get_init_value(param))), expected_prompt=Prompt.CHANGE_PARAM_MENU) if not result: raise InstrumentParameterException("Could not set param %s" % param) self._go_to_root_menu() elif (param == Parameter.METADATA_RESTART): self._navigate(SubMenu.METADATA_RESTART) result = self._do_cmd_resp(Command.DIRECT_SET, (1 + int(self._param_dict.get_init_value(param))), expected_prompt=Prompt.CHANGE_PARAM_MENU) if not result: raise InstrumentParameterException("Could not set param %s" % param) self._go_to_root_menu() elif (param == Parameter.VERBOSE): self._navigate(SubMenu.VERBOSE) result = self._do_cmd_resp(Command.DIRECT_SET, self._param_dict.get_init_value(param), expected_prompt=Prompt.CHANGE_PARAM_MENU) if not result: raise InstrumentParameterException("Could not set param %s" % param) self._go_to_root_menu() elif (param == Parameter.EH_ISOLATION_AMP_POWER): result = self._navigate(SubMenu.EH_ISOLATION_AMP_POWER) while not result: result = self._navigate(SubMenu.EH_ISOLATION_AMP_POWER) elif (param == Parameter.HYDROGEN_POWER): result = self._navigate(SubMenu.HYDROGEN_POWER) while not result: result = self._navigate(SubMenu.HYDROGEN_POWER) elif (param == Parameter.INST_AMP_POWER): result = self._navigate(SubMenu.INST_AMP_POWER) while not result: result = self._navigate(SubMenu.INST_AMP_POWER) elif (param == Parameter.REFERENCE_TEMP_POWER): result = self._navigate(SubMenu.REFERENCE_TEMP_POWER) while not result: result = self._navigate(SubMenu.REFERENCE_TEMP_POWER) elif (param == Parameter.RES_SENSOR_POWER): result = self._navigate(SubMenu.RES_SENSOR_POWER) while not result: result = self._navigate(SubMenu.RES_SENSOR_POWER) # re-sync with param dict? self._go_to_root_menu() self._update_params() # Should be good by now, but let's double check just to be safe for param in self._param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY): if (param == Parameter.VERBOSE): continue if (self._param_dict.get(param) != self._param_dict.get_init_value(param)): raise InstrumentProtocolException("Could not set default values!") def _build_param_dict(self): """ Populate the parameter dictionary with parameters. For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() self._param_dict.add(Parameter.CYCLE_TIME, r'(\d+)\s+= Cycle Time \(.*\)\r\n(0|1)\s+= Minutes or Seconds Cycle Time', lambda match : self._to_seconds(int(match.group(1)), int(match.group(2))), self._int_to_string, visibility=ParameterDictVisibility.READ_WRITE, startup_param=True, direct_access=False, default_value=20, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["1", Prompt.CYCLE_TIME_PROMPT]]) self._param_dict.add(Parameter.VERBOSE, r'', # Write-only, so does it really matter? lambda match : None, self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=1, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["2", Prompt.VERBOSE_PROMPT]]) self._param_dict.add(Parameter.METADATA_POWERUP, r'(0|1)\s+= Metadata Print Status on Power up', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=0, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["3", Prompt.METADATA_PROMPT]]) self._param_dict.add(Parameter.METADATA_RESTART, r'(0|1)\s+= Metadata Print Status on Restart Data Collection', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=0, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["4", Prompt.METADATA_PROMPT]]) self._param_dict.add(Parameter.RES_SENSOR_POWER, r'(0|1)\s+= Res Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["1"]]) self._param_dict.add(Parameter.INST_AMP_POWER, r'(0|1)\s+= Thermocouple & Hydrogen Amp Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["2"]]) self._param_dict.add(Parameter.EH_ISOLATION_AMP_POWER, r'(0|1)\s+= eh Amp Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["3"]]) self._param_dict.add(Parameter.HYDROGEN_POWER, r'(0|1)\s+= Hydrogen Sensor Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["4"]]) self._param_dict.add(Parameter.REFERENCE_TEMP_POWER, r'(0|1)\s+= Reference Temperature Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["5"]]) @staticmethod def _to_seconds(value, unit): """ Converts a number and a unit into seconds. Ie if "4" and "1" comes in, it spits out 240 @param value The int value for some number of minutes or seconds @param unit int of 0 or 1 where 0 is seconds, 1 is minutes @return Number of seconds. """ if (not isinstance(value, int)) or (not isinstance(unit, int)): raise InstrumentProtocolException("Invalid second arguments!") if unit == 1: return value * 60 elif unit == 0: return value else: raise InstrumentProtocolException("Invalid Units!") @staticmethod def _from_seconds(value): """ Converts a number of seconds into a (unit, value) tuple. @param value The number of seconds to convert @retval A tuple of unit and value where the unit is 1 for seconds and 2 for minutes. If the value is 15-59, units should be returned in seconds. If the value is over 59, the units will be returned in a number of minutes where the seconds are rounded down to the nearest minute. """ if (value < 15) or (value > 3600): raise InstrumentParameterException("Invalid seconds value: %s" % value) if (value < 60): return (1, value) else: return (2, value // 60)
class TestUnitProtocolParameterDict(MiUnitTestCase): @staticmethod def pick_byte2(input): """ Get the 2nd byte as an example of something tricky and arbitrary""" val = int(input) >> 8 val = val & 255 return val """ Test cases for instrument driver class. Functions in this class provide instrument driver unit tests and provide a tutorial on use of the driver interface. """ def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("baz", r'.*baz=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS) self.param_dict.add("bat", r'.*bat=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY) def test_get_direct_access_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_direct_access_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 2) self.assert_("foo" in result) self.assert_("baz" in result) def test_get_startup_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_startup_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 2) self.assert_("foo" in result) self.assert_("bar" in result) def test_set_default(self): """ Test setting a default value """ result = self.param_dict.get_config() self.assertEquals(result["foo"], None) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.param_dict.update("foo=1000") self.assertEquals(self.param_dict.get("foo"), 1000) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.assertRaises(ValueError, self.param_dict.set_default, "qux") def test_update_many(self): """ Test updating of multiple variables from the same input """ sample_input = """ foo=100 bar=200, baz=300 """ self.assertNotEquals(self.param_dict.get("foo"), 100) self.assertNotEquals(self.param_dict.get("bar"), 200) self.assertNotEquals(self.param_dict.get("baz"), 300) result = self.param_dict.update_many(sample_input) log.debug("result: %s", result) self.assertEquals(result["foo"], True) self.assertEquals(result["bar"], True) self.assertEquals(result["baz"], True) self.assertEquals(self.param_dict.get("foo"), 100) self.assertEquals(self.param_dict.get("bar"), 200) self.assertEquals(self.param_dict.get("baz"), 300) def test_visibility_list(self): lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_WRITE) self.assertEquals(lst, ["foo", "bar"]) lst = self.param_dict.get_visibility_list(ParameterDictVisibility.DIRECT_ACCESS) self.assertEquals(lst, ["baz"]) lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY) self.assertEquals(lst, ["bat", "qux"]) def test_function_values(self): """ Make sure we can add and update values with functions instead of patterns """ self.param_dict.add_paramdictval( FunctionParamDictVal( "fn_foo", self.pick_byte2, lambda x : str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add_paramdictval( FunctionParamDictVal( "fn_bar", lambda x : bool(x&2), # bit map example lambda x : str(x), direct_access=True, startup_param=True, value=False, visibility=ParameterDictVisibility.READ_WRITE) ) # check defaults just to be safe val = self.param_dict.get("fn_foo") self.assertEqual(val, 1) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) result = self.param_dict.update(1005) # just change first in list val = self.param_dict.get("fn_foo") self.assertEqual(val, 3) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # fn_bar does not get updated here result = self.param_dict.update_many(1205) self.assertEqual(result['fn_foo'], True) self.assertEqual(len(result), 1) val = self.param_dict.get("fn_foo") self.assertEqual(val, 4) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # both are updated now result = self.param_dict.update_many(6) self.assertEqual(result['fn_foo'], True) self.assertEqual(result['fn_bar'], True) self.assertEqual(len(result), 2) val = self.param_dict.get("fn_foo") self.assertEqual(val, 0) val = self.param_dict.get("fn_bar") self.assertEqual(val, True) def test_mixed_pdv_types(self): """ Verify we can add different types of PDVs in one container """ self.param_dict.add_paramdictval( FunctionParamDictVal( "fn_foo", self.pick_byte2, lambda x : str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add_paramdictval( RegexParamDictVal("foo", r'.*foo=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=True, startup_param=True, value=10, visibility=ParameterDictVisibility.READ_WRITE) ) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), direct_access=False, startup_param=True, value=15, visibility=ParameterDictVisibility.READ_WRITE) self.assertEqual(self.param_dict.get("fn_foo"), 1) self.assertEqual(self.param_dict.get("foo"), 10) self.assertEqual(self.param_dict.get("bar"), 15) def test_base_update(self): pdv = ParameterDictVal("foo", lambda x : str(x), value=12) self.assertEqual(pdv.value, 12) result = pdv.update(1) self.assertEqual(result, True) self.assertEqual(pdv.value, 1) # Its a base class...monkey see, monkey do result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.value, "foo=1") def test_regex_val(self): pdv = RegexParamDictVal("foo", r'.*foo=(\d+).*', lambda match : int(match.group(1)), lambda x : str(x), value=12) self.assertEqual(pdv.value, 12) result = pdv.update(1) self.assertEqual(result, False) self.assertEqual(pdv.value, 12) result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.value, 1) def test_function_val(self): pdv = FunctionParamDictVal("foo", self.pick_byte2, lambda x : str(x), value=12) self.assertEqual(pdv.value, 12) self.assertRaises(TypeError, pdv.update(1)) result = pdv.update("1205") self.assertEqual(pdv.value, 4) self.assertEqual(result, True) def test_set_init_value(self): result = self.param_dict.get("foo") self.assertEqual(result, None) self.param_dict.set_init_value("foo", 42) result = self.param_dict.get_init_value("foo") self.assertEqual(result, 42)
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=150, units='mL', startup_param=True, display_name="flush_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="flush_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="flush_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=4000, units='mL', startup_param=True, display_name="fill_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="fill_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="fill_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL', startup_param=True, display_name="clear_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="clear_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="clear_min_flow", visibility=ParameterDictVisibility.IMMUTABLE)
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ last_sample = '' def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_SAMPLE, self._handler_acquire_sample) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.CLOCK_SYNC, self._handler_command_sync_clock) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.FLASH_STATUS, self._handler_flash_status) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ENTER, self._handler_autosample_enter) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.EXIT, self._handler_autosample_exit) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ACQUIRE_SAMPLE, self._handler_acquire_sample) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.CLOCK_SYNC, self._handler_autosample_sync_clock) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.FLASH_STATUS, self._handler_flash_status) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status) # We setup a new state for clock sync because then we could use the state machine so the autosample scheduler # is disabled before we try to sync the clock. Otherwise there could be a race condition introduced when we # are syncing the clock and the scheduler requests a sample. self._protocol_fsm.add_handler(ProtocolState.SYNC_CLOCK, ProtocolEvent.ENTER, self._handler_sync_clock_enter) self._protocol_fsm.add_handler(ProtocolState.SYNC_CLOCK, ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock_sync) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct) # Add build handlers for device commands. self._add_build_handler(Command.GET_CLOCK, self._build_simple_command) self._add_build_handler(Command.SET_CLOCK, self._build_set_clock_command) self._add_build_handler(Command.D, self._build_simple_command) self._add_build_handler(Command.GO, self._build_simple_command) self._add_build_handler(Command.STOP, self._build_simple_command) self._add_build_handler(Command.FS, self._build_simple_command) self._add_build_handler(Command.STAT, self._build_simple_command) # Add response handlers for device commands. self._add_response_handler(Command.GET_CLOCK, self._parse_clock_response) self._add_response_handler(Command.SET_CLOCK, self._parse_clock_response) self._add_response_handler(Command.FS, self._parse_fs_response) self._add_response_handler(Command.STAT, self._parse_common_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, ProtocolEvent.ACQUIRE_STATUS) self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(METBK_SampleDataParticle.regex_compiled()) matchers.append(METBK_StatusDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s" %chunk) self._extract_sample(METBK_SampleDataParticle, METBK_SampleDataParticle.regex_compiled(), chunk, timestamp) self._extract_sample(METBK_StatusDataParticle, METBK_StatusDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # override methods from base class. ######################################################################## def _extract_sample(self, particle_class, regex, line, timestamp, publish=True): """ Overridden to add duplicate sample checking. This duplicate checking should only be performed on sample chunks and not other chunk types, therefore the regex is performed before the string checking. Extract sample from a response line if present and publish parsed particle @param particle_class The class to instantiate for this specific data particle. Parameterizing this allows for simple, standard behavior from this routine @param regex The regular expression that matches a data sample @param line string to match for sample. @param timestamp port agent timestamp to include with the particle @param publish boolean to publish samples (default True). If True, two different events are published: one to notify raw data and the other to notify parsed data. @retval dict of dicts {'parsed': parsed_sample, 'raw': raw_sample} if the line can be parsed for a sample. Otherwise, None. @todo Figure out how the agent wants the results for a single poll and return them that way from here """ sample = None match = regex.match(line) if match: if particle_class == METBK_SampleDataParticle: # check to see if there is a delta from last sample, and don't parse this sample if there isn't if match.group(0) == self.last_sample: return # save this sample as last_sample for next check self.last_sample = match.group(0) particle = particle_class(line, port_timestamp=timestamp) parsed_sample = particle.generate() if publish and self._driver_event: self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample) sample = json.loads(parsed_sample) return sample return sample ######################################################################## # implement virtual methods from base class. ######################################################################## def apply_startup_params(self): """ Apply sample_interval startup parameter. """ config = self.get_startup_config() log.debug("apply_startup_params: startup config = %s" %config) if config.has_key(Parameter.SAMPLE_INTERVAL): log.debug("apply_startup_params: setting sample_interval to %d" %config[Parameter.SAMPLE_INTERVAL]) self._param_dict.set_value(Parameter.SAMPLE_INTERVAL, config[Parameter.SAMPLE_INTERVAL]) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ (protocol_state, agent_state) = self._discover() # If we are just starting up and we land in command mode then our state should # be idle if(agent_state == ResourceAgentState.COMMAND): agent_state = ResourceAgentState.IDLE log.debug("_handler_unknown_discover: state = %s", protocol_state) return (protocol_state, agent_state) ######################################################################## # Clock Sync handlers. # Not much to do in this state except sync the clock then transition # back to autosample. When in command mode we don't have to worry about # stopping the scheduler so we just sync the clock without state # transitions ######################################################################## def _handler_sync_clock_enter(self, *args, **kwargs): """ Enter sync clock state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._protocol_fsm.on_event(ProtocolEvent.CLOCK_SYNC) def _handler_sync_clock_sync(self, *args, **kwargs): """ Sync the clock """ next_state = ProtocolState.AUTOSAMPLE next_agent_state = ResourceAgentState.STREAMING result = None self._sync_clock() self._async_agent_state_change(ResourceAgentState.STREAMING) return(next_state,(next_agent_state, result)) ######################################################################## # Command handlers. # just implemented to make DA possible, instrument has no actual command mode ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ self._init_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ next_state = None result = None return (next_state, result) def _handler_command_start_direct(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.DIRECT_ACCESS next_agent_state = ResourceAgentState.DIRECT_ACCESS return (next_state, (next_agent_state, result)) def _handler_command_start_autosample(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.AUTOSAMPLE next_agent_state = ResourceAgentState.STREAMING self._start_logging() return (next_state, (next_agent_state, result)) def _handler_command_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None self._sync_clock() return(next_state,(next_agent_state, result)) ######################################################################## # autosample handlers. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Enter autosample state Because this is an instrument that must be polled we need to ensure the scheduler is added when we are in an autosample state. This scheduler raises events to poll the instrument for data. """ self._init_params() self._ensure_autosample_config() self._add_scheduler_event(AUTO_SAMPLE_SCHEDULED_JOB, ProtocolEvent.ACQUIRE_SAMPLE) # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_autosample_exit(self, *args, **kwargs): """ exit autosample state. """ self._remove_scheduler(AUTO_SAMPLE_SCHEDULED_JOB) def _handler_autosample_stop_autosample(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND self._stop_logging() return (next_state, (next_agent_state, result)) def _handler_autosample_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = ProtocolState.SYNC_CLOCK next_agent_state = ResourceAgentState.BUSY result = None return(next_state,(next_agent_state, result)) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ pass def _handler_direct_access_execute_direct(self, data): next_state = None result = None self._do_cmd_direct(data) return (next_state, result) def _handler_direct_access_stop_direct(self): result = None (next_state, next_agent_state) = self._discover() return (next_state, (next_agent_state, result)) ######################################################################## # general handlers. ######################################################################## def _handler_flash_status(self, *args, **kwargs): """ Acquire flash status from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None result = self._do_cmd_resp(Command.FS, expected_prompt=Prompt.FS) log.debug("FLASH RESULT: %s", result) return (next_state, (next_agent_state, result)) def _handler_acquire_sample(self, *args, **kwargs): """ Acquire sample from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None result = self._do_cmd_resp(Command.D, *args, **kwargs) return (next_state, (next_agent_state, result)) def _handler_acquire_status(self, *args, **kwargs): """ Acquire status from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None log.debug( "Logging status: %s", self._is_logging()) result = self._do_cmd_resp(Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.GO]) return (next_state, (next_agent_state, result)) ######################################################################## # Private helpers. ######################################################################## def _set_params(self, *args, **kwargs): """ Overloaded from the base class, used in apply DA params. Not needed here so just noop it. """ pass def _discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ logging = self._is_logging() if(logging == True): protocol_state = ProtocolState.AUTOSAMPLE agent_state = ResourceAgentState.STREAMING elif(logging == False): protocol_state = ProtocolState.COMMAND agent_state = ResourceAgentState.COMMAND else: protocol_state = ProtocolState.UNKNOWN agent_state = ResourceAgentState.ACTIVE_UNKNOWN return (protocol_state, agent_state) def _start_logging(self): """ start the instrument logging if is isn't running already. """ if(not self._is_logging()): log.debug("Sending start logging command: %s", Command.GO) self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO) def _stop_logging(self): """ stop the instrument logging if is is running. When the instrument is in a syncing state we can not stop logging. We must wait before we sent the stop command. """ if(self._is_logging()): log.debug("Attempting to stop the instrument logging.") result = self._do_cmd_resp(Command.STOP, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) log.debug("Stop Command Result: %s", result) # If we are still logging then let's wait until we are not # syncing before resending the command. if(self._is_logging()): self._wait_for_sync() log.debug("Attempting to stop the instrument again.") result = self._do_cmd_resp(Command.STOP, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) log.debug("Stop Command Result: %s", result) def _wait_for_sync(self): """ When the instrument is syncing internal parameters we can't stop logging. So we will watch the logging status and when it is not synchronizing we will return. Basically we will just block until we are no longer syncing. @raise InstrumentProtocolException when we timeout waiting for a transition. """ timeout = time.time() + SYNC_TIMEOUT while(time.time() < timeout): result = self._do_cmd_resp(Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) match = LOGGING_SYNC_COMPILED.match(result) if(match): log.debug("We are still in sync mode. Wait a bit and retry") time.sleep(2) else: log.debug("Transitioned out of sync.") return True # We timed out raise InstrumentProtocolException("failed to transition out of sync mode") def _is_logging(self): """ Run the status command to determine if we are in command or autosample mode. @return: True if sampling, false if not, None if we can't determine """ log.debug("_is_logging: start") result = self._do_cmd_resp(Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.GO]) log.debug("Checking logging status from %s", result) match = LOGGING_STATUS_COMPILED.match(result) if not match: log.error("Unable to determine logging status from: %s", result) return None if match.group(1) == 'GO': log.debug("Looks like we are logging: %s", match.group(1)) return True else: log.debug("Looks like we are NOT logging: %s", match.group(1)) return False def _ensure_autosample_config(self): scheduler_config = self._get_scheduler_config() if (scheduler_config == None): log.debug("_ensure_autosample_config: adding scheduler element to _startup_config") self._startup_config[DriverConfigKey.SCHEDULER] = {} scheduler_config = self._get_scheduler_config() log.debug("_ensure_autosample_config: adding autosample config to _startup_config") config = {DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.SAMPLE_INTERVAL)}} self._startup_config[DriverConfigKey.SCHEDULER][AUTO_SAMPLE_SCHEDULED_JOB] = config if(not self._scheduler): self.initialize_scheduler() def _sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None time_format = "%Y/%m/%d %H:%M:%S" str_val = get_timestamp_delayed(time_format) log.debug("Setting instrument clock to '%s'", str_val) self._do_cmd_resp(Command.SET_CLOCK, str_val, expected_prompt=Prompt.CR_NL) def _wakeup(self, timeout): """There is no wakeup sequence for this instrument""" pass def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="start autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="stop autosample") self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="synchronize clock") self._cmd_dict.add(Capability.ACQUIRE_STATUS, display_name="acquire status") self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="acquire sample") self._cmd_dict.add(Capability.FLASH_STATUS, display_name="flash status") def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.CLOCK, r'(.*)\r\n', lambda match : match.group(1), lambda string : str(string), type=ParameterDictType.STRING, display_name="clock", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add(Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ log.debug("_update_params:") # Issue clock command and parse results. # This is the only parameter and it is always changing so don't bother with the 'change' event self._do_cmd_resp(Command.GET_CLOCK) def _build_set_clock_command(self, cmd, val): """ Build handler for set clock command (cmd=val followed by newline). @param cmd the string for setting the clock (this should equal #CLOCK=). @param val the parameter value to set. @ retval The set command to be sent to the device. """ cmd = '%s%s' %(cmd, val) + NEWLINE return cmd def _parse_clock_response(self, response, prompt): """ Parse handler for clock command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if clock command misunderstood. """ log.debug("_parse_clock_response: response=%s, prompt=%s" %(response, prompt)) if prompt not in [Prompt.CR_NL]: raise InstrumentProtocolException('CLOCK command not recognized: %s.' % response) if not self._param_dict.update(response): raise InstrumentProtocolException('CLOCK command not parsed: %s.' % response) return def _parse_fs_response(self, response, prompt): """ Parse handler for FS command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if FS command misunderstood. """ log.debug("_parse_fs_response: response=%s, prompt=%s" %(response, prompt)) if prompt not in [Prompt.FS]: raise InstrumentProtocolException('FS command not recognized: %s.' % response) return response def _parse_common_response(self, response, prompt): """ Parse handler for common commands. @param response command response string. @param prompt prompt following command response. """ return response
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add( Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, default_value=DEFAULT_SAMPLE_RATE, startup_param=True, display_name='D1000 sample periodicity (sec)', visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, display_name='base channel address', type=ParameterDictType.INT, default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, display_name='line feed flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, display_name='parity type', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, display_name='parity flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, display_name='extended addressing', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, display_name='baud rate', type=ParameterDictType.INT, default_value=9600) self._add_setup_param(Parameter.ALARM_ENABLE, bool, display_name='enable alarms', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, display_name='low alarm latching', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, display_name='high alarm latching', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, display_name='4 wire RTD flag', type=ParameterDictType.BOOL, default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, display_name='Fahrenheit flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.ECHO, bool, display_name='daisy chain', type=ParameterDictType.BOOL, default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, display_name='communication delay', type=ParameterDictType.INT, default_value=0) self._add_setup_param(Parameter.PRECISION, int, display_name='precision', type=ParameterDictType.INT, default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, display_name='large signal filter constant', type=ParameterDictType.FLOAT, default_value=0.0) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, display_name='small signal filter constant', type=ParameterDictType.FLOAT, default_value=0.50) for key in self._param_dict.get_keys(): self._param_dict.set_default(key)
def _build_param_dict(self): """ Populate the parameter dictionary with parameters. For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() self._param_dict.add(Parameter.LAUNCH_TIME, CONFIGURATION_REGEX, lambda match: int(match.group(1), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='launch time') self._param_dict.add(Parameter.START_TIME_FROM_LAUNCH, CONFIGURATION_REGEX, lambda match: int(match.group(2), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02C7EA00, visibility=ParameterDictVisibility.READ_ONLY, display_name='start time after launch time') self._param_dict.add(Parameter.STOP_TIME_FROM_START, CONFIGURATION_REGEX, lambda match: int(match.group(3), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01E13380, visibility=ParameterDictVisibility.READ_ONLY, display_name='stop time after start time') self._param_dict.add(Parameter.MODE_BITS, CONFIGURATION_REGEX, lambda match: int(match.group(4), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0A, visibility=ParameterDictVisibility.READ_ONLY, display_name='mode bits (set to 00001010)') self._param_dict.add(Parameter.SAMI_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(5), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami sample interval') self._param_dict.add(Parameter.SAMI_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(6), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x04, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami driver version') self._param_dict.add(Parameter.SAMI_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(7), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami parameter pointer') self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(8), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 sample interval') self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(9), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 driver version') self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(10), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0B, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 parameter pointer') self._param_dict.add(Parameter.DEVICE2_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(11), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 sample interval') self._param_dict.add(Parameter.DEVICE2_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(12), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 driver version') self._param_dict.add(Parameter.DEVICE2_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(13), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 parameter pointer') self._param_dict.add(Parameter.DEVICE3_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(14), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 sample interval') self._param_dict.add(Parameter.DEVICE3_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(15), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 driver version') self._param_dict.add(Parameter.DEVICE3_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(16), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 parameter pointer') self._param_dict.add(Parameter.PRESTART_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(17), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart sample interval') self._param_dict.add(Parameter.PRESTART_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(18), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart driver version') self._param_dict.add(Parameter.PRESTART_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(19), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart parameter pointer') self._param_dict.add(Parameter.GLOBAL_CONFIGURATION, CONFIGURATION_REGEX, lambda match: int(match.group(20), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='global bits (set to 00000111)') self._param_dict.add(Parameter.NUMBER_SAMPLES_AVERAGED, CONFIGURATION_REGEX, lambda match: int(match.group(21), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_FLUSHES, CONFIGURATION_REGEX, lambda match: int(match.group(22), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(23), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(24), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_REAGENT_PUMPS, CONFIGURATION_REGEX, lambda match: int(match.group(25), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.VALVE_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(26), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_IND, CONFIGURATION_REGEX, lambda match: int(match.group(27), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PV_OFF_IND, CONFIGURATION_REGEX, lambda match: int(match.group(28), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_BLANKS, CONFIGURATION_REGEX, lambda match: int(match.group(29), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_MEASURE_T, CONFIGURATION_REGEX, lambda match: int(match.group(30), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_TO_MEASURE, CONFIGURATION_REGEX, lambda match: int(match.group(31), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.MEASURE_TO_PUMP_ON, CONFIGURATION_REGEX, lambda match: int(match.group(32), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_MEASUREMENTS, CONFIGURATION_REGEX, lambda match: int(match.group(33), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.SALINITY_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(34), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='')
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. handlers = { ProtocolState.UNKNOWN: [ (ProtocolEvent.ENTER, self._handler_unknown_enter), (ProtocolEvent.EXIT, self._handler_unknown_exit), (ProtocolEvent.DISCOVER, self._handler_unknown_discover), ], ProtocolState.COMMAND: [ (ProtocolEvent.ENTER, self._handler_command_enter), (ProtocolEvent.EXIT, self._handler_command_exit), (ProtocolEvent.START_DIRECT, self._handler_command_start_direct), (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_sample), (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_autosample), (ProtocolEvent.GET, self._handler_get), (ProtocolEvent.SET, self._handler_command_set), ], ProtocolState.AUTOSAMPLE: [ (ProtocolEvent.ENTER, self._handler_autosample_enter), (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_sample), (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop), (ProtocolEvent.EXIT, self._handler_autosample_exit), ], ProtocolState.DIRECT_ACCESS: [ (ProtocolEvent.ENTER, self._handler_direct_access_enter), (ProtocolEvent.EXIT, self._handler_direct_access_exit), (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct), (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct), ], } for state in handlers: for event, handler in handlers[state]: self._protocol_fsm.add_handler(state, event, handler) # Add build handlers for device commands - we are only using simple commands for cmd in Command.list(): self._add_build_handler(cmd, self._build_command) self._add_response_handler(cmd, self._check_command) self._add_build_handler(Command.SETUP, self._build_setup_command) self._add_response_handler(Command.READ_SETUP, self._read_setup_response_handler) # Add response handlers for device commands. # self._add_response_handler(Command.xyz, self._parse_xyz_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) self._sent_cmds = None self.initialize_scheduler() # unit identifiers - must match the setup command (SU31 - '1') self._units = ['1', '2', '3'] self._setup = None # set by the read setup command handler for comparison to see if the config needs reset @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(D1000TemperatureDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) if not return_list: log.debug("sieve_function: raw_data=%r, return_list=%s", raw_data, return_list) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s", chunk) self._extract_sample(D1000TemperatureDataParticle, D1000TemperatureDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # implement virtual methods from base class. ######################################################################## def _set_params(self, *args, **kwargs): """ Issue commands to the instrument to set various parameters. If startup is set to true that means we are setting startup values and immutable parameters can be set. Otherwise only READ_WRITE parameters can be set. must be overloaded in derived classes @param params dictionary containing parameter name and value pairs @param startup - a flag, true indicates initializing, false otherwise """ params = args[0] # check for attempt to set readonly parameters (read-only or immutable set outside startup) self._verify_not_readonly(*args, **kwargs) old_config = self._param_dict.get_config() for (key, val) in params.iteritems(): log.debug("KEY = " + str(key) + " VALUE = " + str(val)) self._param_dict.set_value(key, val) new_config = self._param_dict.get_config() # check for parameter change if not dict_equal(old_config, new_config): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def apply_startup_params(self): """ Apply startup parameters """ config = self.get_startup_config() for param in Parameter.list(): if param in config: self._param_dict.set_value(param, config[param]) ######################################################################## # Private helpers. ######################################################################## def _wakeup(self, wakeup_timeout=10, response_timeout=3): """ Over-ridden because the D1000 does not go to sleep and requires no special wake-up commands. @param wakeup_timeout The timeout to wake the device. @param response_timeout The time to look for response to a wakeup attempt. @throw InstrumentTimeoutException if the device could not be woken. """ pass def _do_command(self, cmd, unit, **kwargs): """ Send command and ensure it matches appropriate response. Simply enforces sending the unit identifier as a required argument. @param cmd - Command to send to instrument @param unit - unit identifier @retval - response from instrument """ self._do_cmd_resp(cmd, unit, write_delay=INTER_CHARACTER_DELAY, **kwargs) def _build_command(self, cmd, unit): """ @param cmd - Command to process @param unit - unit identifier """ return '#' + unit + cmd + NEWLINE def _build_setup_command(self, cmd, unit): """ @param cmd - command to send - should be 'SU' @param unit - unit identifier - should be '1', '2', or '3', must be a single character """ # use defaults - in the future, may consider making some of these parameters # byte 0 channel_address = unit # byte 1 line_feed = self._param_dict.format(Parameter.LINEFEED) parity_type = self._param_dict.format(Parameter.PARITY_TYPE) parity_enable = self._param_dict.format(Parameter.PARITY_ENABLE) extended_addressing = self._param_dict.format(Parameter.EXTENDED_ADDRESSING) baud_rate = self._param_dict.format(Parameter.BAUD_RATE) baud_rate = getattr(BaudRate, 'BAUD_%d' % baud_rate, BaudRate.BAUD_9600) # byte 2 alarm_enable = self._param_dict.format(Parameter.ALARM_ENABLE) low_alarm_latch = self._param_dict.format(Parameter.LOW_ALARM_LATCH) high_alarm_latch = self._param_dict.format(Parameter.HIGH_ALARM_LATCH) rtd_wire = self._param_dict.format(Parameter.RTD_4_WIRE) temp_units = self._param_dict.format(Parameter.TEMP_UNITS) echo = self._param_dict.format(Parameter.ECHO) delay_units = self._param_dict.format(Parameter.COMMUNICATION_DELAY) # byte 3 precision = self._param_dict.format(Parameter.PRECISION) precision = getattr(UnitPrecision, 'DIGITS_%d' % precision, UnitPrecision.DIGITS_6) large_signal_filter_constant = self._param_dict.format(Parameter.LARGE_SIGNAL_FILTER_C) large_signal_filter_constant = filter_enum(large_signal_filter_constant) small_signal_filter_constant = self._param_dict.format(Parameter.SMALL_SIGNAL_FILTER_C) small_signal_filter_constant = filter_enum(small_signal_filter_constant) # # Factory default: 0x31070182 # # Lab default: 0x310214C2 byte_0 = int(channel_address.encode("hex"), 16) log.debug('byte 0: %s', byte_0) byte_1 = \ (line_feed << 7) + \ (parity_type << 6) + \ (parity_enable << 5) + \ (extended_addressing << 4) + \ baud_rate log.debug('byte 1: %s', byte_1) byte_2 = \ (alarm_enable << 7) + \ (low_alarm_latch << 6) + \ (high_alarm_latch << 5) + \ (rtd_wire << 4) + \ (temp_units << 3) + \ (echo << 2) + \ delay_units log.debug('byte 2: %s', byte_2) byte_3 = \ (precision << 6) + \ (large_signal_filter_constant << 3) + \ small_signal_filter_constant log.debug('byte 3: %s', byte_3) setup_command = '#%sSU%02x%02x%02x%02x' % (unit[0], byte_0, byte_1, byte_2, byte_3) + NEWLINE log.debug('default setup command (%r) for unit %02x (%s)' % (setup_command, byte_0, unit[0])) return setup_command def _check_command(self, resp, prompt): """ Perform a checksum calculation on provided data. The checksum used for comparison is the last two characters of the line. @param resp - response from the instrument to the command @param prompt - expected prompt (or the joined groups from a regex match) @retval """ for line in resp.split(NEWLINE): if line.startswith('?'): raise InstrumentProtocolException('error processing command (%r)', resp[1:]) if line.startswith('*'): # response if not valid_response(line): raise InstrumentProtocolException('checksum failed (%r)', line) def _read_setup_response_handler(self, resp, prompt): """ Save the setup. @param resp - response from the instrument to the command @param prompt - expected prompt (or the joined groups from a regex match) """ self._check_command(resp, prompt) self._setup = resp def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with commands. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample") self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="Acquire Sample") self._cmd_dict.add(Capability.DISCOVER, display_name='Discover') def _add_setup_param(self, name, fmt, **kwargs): """ Add setup command to the parameter dictionary. All 'SU' parameters are not startup parameter, but should be restored upon return from direct access. These parameters are all part of the instrument command 'SU'. """ self._param_dict.add(name, '', None, fmt, startup_param=False, direct_access=True, visibility=ParameterDictVisibility.READ_ONLY, **kwargs) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, startup_param=True, display_name='D1000 Sample Periodicity', description='Periodicity of D1000 temperature sample in autosample mode: (1-3600)', default_value=DEFAULT_SAMPLE_RATE, units=Units.SECOND, visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, type=ParameterDictType.INT, display_name='Base Channel Address', description='Hex value of ASCII character to ID unit, e.g. 31 is the ASCII code for 1: (30-31, 41-5A, 61-7A)', default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, type=ParameterDictType.BOOL, display_name='Line Feed Flag', description='Enable D1000 to generate a linefeed before and after each response: (true | false)', default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, type=ParameterDictType.BOOL, display_name='Parity Type', description='Sets the parity: (true:odd | false:even)', default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Parity Flag', description='Enable use of parity bit, a parity error will be issued if detected: (true | false)', default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, type=ParameterDictType.BOOL, display_name='Extended Addressing', description='Enable extended addressing: (true | false)', default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, type=ParameterDictType.INT, display_name='Baud Rate', description='Using ethernet interface in deployed configuration: (300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600)', default_value=9600, units=Units.BAUD) self._add_setup_param(Parameter.ALARM_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Enable Alarms', description='Enable alarms to be controlled by the Digital Output (DO) command: (true | false)', default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='Low Alarm Latching', description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='High Alarm Latching', description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, type=ParameterDictType.BOOL, display_name='4 Wire RTD Flag', description='Represents a physical configuration of the instrument, disabling may cause data to be misaligned: (true | false)', default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, type=ParameterDictType.BOOL, display_name='Fahrenheit Flag', description='Flag to control the temperature format: (true:Fahrenheit | false:Celsius)', default_value=False) self._add_setup_param(Parameter.ECHO, bool, type=ParameterDictType.BOOL, display_name='Daisy Chain', description='If not set, only 1 out of 3 D1000s will process commands: (true | false)', default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, type=ParameterDictType.INT, display_name='Communication Delay', description='The number of delays to add when processing commands: (0-3)', default_value=0) self._add_setup_param(Parameter.PRECISION, int, type=ParameterDictType.INT, display_name='Precision', description='Number of digits the instrument should output for temperature query: (4-7)', default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Large Signal Filter Constant', description='Time to reach 63% of its final value: (0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0)', default_value=0.0, units=Units.SECOND) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Small Signal Filter Constant', description='Smaller filter constant, should be larger than large filter constant: (0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0)', default_value=0.50, units=Units.SECOND) for key in self._param_dict.get_keys(): self._param_dict.set_default(key) def _update_params(self): """ Update the parameter dictionary. """ pass def _restore_params(self): """ Restore D1000, clearing any alarms and set-point. """ # make sure the alarms are disabled - preferred over doing setup, then clear alarms commands self._param_dict.set_value(Parameter.ALARM_ENABLE, False) for i in self._units: current_setup = None # set in READ_SETUP response handler try: self._do_command(Command.READ_SETUP, i, response_regex=Response.READ_SETUP) current_setup = self._setup[4:][:-2] # strip off the leader and checksum except InstrumentTimeoutException: log.error('D1000 unit %s has been readdressed, unable to restore settings' % i[0]) new_setup = self._build_setup_command(Command.SETUP, i)[4:] # strip leader (no checksum) if not current_setup == new_setup: log.debug('restoring setup to default state (%s) from current state (%s)', new_setup, current_setup) self._do_command(Command.ENABLE_WRITE, i) self._do_command(Command.SETUP, i) self._do_command(Command.ENABLE_WRITE, i) self._do_command(Command.CLEAR_ZERO, i) ######################################################################## # Event handlers for UNKNOWN state. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state @retval (next_state, current state), (ProtocolState.COMMAND, None) if successful. """ # force to command mode, this instrument has no autosample mode next_state = ProtocolState.COMMAND result = ResourceAgentState.COMMAND return ProtocolState.COMMAND, ResourceAgentState.IDLE ######################################################################## # Event handlers for COMMAND state. ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ # Command device to update parameters and send a config change event if needed. self._restore_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ input_params = args[0] for key, value in input_params.items(): if not Parameter.has(key): raise InstrumentParameterException('Invalid parameter supplied to set: %s' % key) try: value = int(value) except TypeError: raise InstrumentParameterException('Invalid value [%s] for parameter %s' % (value, key)) if key == Parameter.SAMPLE_INTERVAL: if value < MIN_SAMPLE_RATE or value > MAX_SAMPLE_RATE: raise InstrumentParameterException('Parameter %s value [%d] is out of range [%d %d]' % (key, value, MIN_SAMPLE_RATE, MAX_SAMPLE_RATE)) startup = False try: startup = args[1] except IndexError: pass self._set_params(input_params, startup) return None, None # return None, (None, None) def _handler_command_autosample(self, *args, **kwargs): """ Begin autosample. """ return ProtocolState.AUTOSAMPLE, (ResourceAgentState.STREAMING, None) def _handler_command_start_direct(self, *args, **kwargs): """ """ return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS, None) ######################################################################## # Event handlers for AUTOSAMPLE state. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Start auto polling the temperature sensors. """ self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._protocol_fsm.on_event(ProtocolEvent.ACQUIRE_SAMPLE) job_name = ScheduledJob.SAMPLE config = { DriverConfigKey.SCHEDULER: { job_name: { DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.SAMPLE_INTERVAL) } } } } self.set_init_params(config) self._add_scheduler_event(ScheduledJob.SAMPLE, ProtocolEvent.ACQUIRE_SAMPLE) def _handler_autosample_exit(self, *args, **kwargs): """ Stop autosampling - remove the scheduled autosample """ if self._scheduler is not None: try: self._remove_scheduler(ScheduledJob.SAMPLE) except KeyError: log.debug('_remove_scheduler count not find: %s', ScheduledJob.SAMPLE) def _handler_sample(self, *args, **kwargs): """ Poll the three temperature probes for current temperature readings. """ for i in self._units: self._do_command(Command.READ, i) return None, (None, None) def _handler_autosample_stop(self, *args, **kwargs): """ Terminate autosampling """ return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ def _handler_direct_access_execute_direct(self, data): self._do_cmd_direct(data) return None, (None, None) def _handler_direct_access_stop_direct(self, *args, **kwargs): result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND # return next_state, (next_agent_state, result) return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)
def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("baz", r'.*baz=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS, get_timeout=30, set_timeout=40, display_name="Baz", description="The baz parameter", type=ParameterDictType.INT, units="nano-bazers", value_description="Should be an integer between 2 and 2000") self.param_dict.add("bat", r'.*bat=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY, get_timeout=10, set_timeout=20, display_name="Bat", description="The bat parameter", type=ParameterDictType.INT, units="nano-batbit", value_description="Should be an integer between 1 and 1000") self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("pho", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("dil", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("qut", r'.*qut=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=[10, 100], visibility=ParameterDictVisibility.DIRECT_ACCESS, expiration=1, get_timeout=10, set_timeout=20, display_name="Qut", description="The qut list parameter", type=ParameterDictType.LIST, units="nano-qutters", value_description="Should be a 2-10 element list of integers between 2 and 2000") self.target_schema = { "bar": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 15 }, "visibility": "READ_WRITE", "range": None, }, "bat": { "description": "The bat parameter", "direct_access": False, "display_name": "Bat", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": 20, "description": "Should be an integer between 1 and 1000", "type": "int", "units": "nano-batbit" }, "visibility": "READ_ONLY", "range": None, }, "baz": { "description": "The baz parameter", "direct_access": True, "display_name": "Baz", "get_timeout": 30, "set_timeout": 40, "startup": False, "value": { "default": 20, "description": "Should be an integer between 2 and 2000", "type": "int", "units": "nano-bazers" }, "visibility": "DIRECT_ACCESS", "range": None, }, "dil": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "foo": { "direct_access": True, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 10 }, "visibility": "READ_WRITE", "range": None, }, "pho": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "qut": { "description": "The qut list parameter", "direct_access": True, "display_name": "Qut", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": [ 10, 100 ], "description": "Should be a 2-10 element list of integers between 2 and 2000", "type": "list", "units": "nano-qutters" }, "visibility": "DIRECT_ACCESS", "range": None, }, "qux": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "READ_ONLY", "range": None, } } self.test_yaml = '''
class TestUnitProtocolParameterDict(TestUnitStringsDict): """ Test cases for instrument driver class. Functions in this class provide instrument driver unit tests and provide a tutorial on use of the driver interface. """ __test__ = True @staticmethod def pick_byte2(input_val): """ Get the 2nd byte as an example of something tricky and arbitrary""" val = int(input_val) >> 8 val &= 255 return val def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add( "baz", r'.*baz=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS, get_timeout=30, set_timeout=40, display_name="Baz", description="The baz parameter", type=ParameterDictType.INT, units="nano-bazers", value_description="Should be an integer between 2 and 2000") self.param_dict.add( "bat", r'.*bat=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY, get_timeout=10, set_timeout=20, display_name="Bat", description="The bat parameter", type=ParameterDictType.INT, units="nano-batbit", value_description="Should be an integer between 1 and 1000") self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("pho", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("dil", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add( "qut", r'.*qut=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=[10, 100], visibility=ParameterDictVisibility.DIRECT_ACCESS, expiration=1, get_timeout=10, set_timeout=20, display_name="Qut", description="The qut list parameter", type=ParameterDictType.LIST, units="nano-qutters", value_description= "Should be a 2-10 element list of integers between 2 and 2000") self.target_schema = { "bar": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 15 }, "visibility": "READ_WRITE", "range": None, }, "bat": { "description": "The bat parameter", "direct_access": False, "display_name": "Bat", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": 20, "description": "Should be an integer between 1 and 1000", "type": "int", "units": "nano-batbit" }, "visibility": "READ_ONLY", "range": None, }, "baz": { "description": "The baz parameter", "direct_access": True, "display_name": "Baz", "get_timeout": 30, "set_timeout": 40, "startup": False, "value": { "default": 20, "description": "Should be an integer between 2 and 2000", "type": "int", "units": "nano-bazers" }, "visibility": "DIRECT_ACCESS", "range": None, }, "dil": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "foo": { "direct_access": True, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 10 }, "visibility": "READ_WRITE", "range": None, }, "pho": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "qut": { "description": "The qut list parameter", "direct_access": True, "display_name": "Qut", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": [10, 100], "description": "Should be a 2-10 element list of integers between 2 and 2000", "type": "list", "units": "nano-qutters" }, "visibility": "DIRECT_ACCESS", "range": None, }, "qux": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "READ_ONLY", "range": None, } } self.test_yaml = ''' parameters: { qut: { description: "QutFileDesc", units: "QutFileUnits", value_description: "QutFileValueDesc", type: "QutFileType", display_name: "QutDisplay" }, extra_param: { description: "ExtraFileDesc", units: "ExtraFileUnits", value_description: "ExtraFileValueDesc", type: "ExtraFileType" } } commands: { dummy: stuff } ''' def test_get_direct_access_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_direct_access_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 3) self.assert_("foo" in result) self.assert_("baz" in result) self.assert_("qut" in result) def test_get_startup_list(self): """ Test to see we can get a list of direct access parameters """ result = self.param_dict.get_startup_list() self.assertTrue(isinstance(result, list)) self.assertEquals(len(result), 2) self.assert_("foo" in result) self.assert_("bar" in result) def test_set_default(self): """ Test setting a default value """ result = self.param_dict.get_config() self.assertEquals(result["foo"], None) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.param_dict.update("foo=1000") self.assertEquals(self.param_dict.get("foo"), 1000) self.param_dict.set_default("foo") self.assertEquals(self.param_dict.get("foo"), 10) self.assertRaises(ValueError, self.param_dict.set_default, "qux") def test_update_many(self): """ Test updating of multiple variables from the same input """ sample_input = """ foo=100 bar=200, baz=300 """ self.assertNotEquals(self.param_dict.get("foo"), 100) self.assertNotEquals(self.param_dict.get("bar"), 200) self.assertNotEquals(self.param_dict.get("baz"), 300) result = self.param_dict.update_many(sample_input) log.debug("result: %s", result) self.assertEquals(result["foo"], True) self.assertEquals(result["bar"], True) self.assertEquals(result["baz"], True) self.assertEquals(self.param_dict.get("foo"), 100) self.assertEquals(self.param_dict.get("bar"), 200) self.assertEquals(self.param_dict.get("baz"), 300) def test_update_specific_values(self): """ test to verify we can limit update to a specific set of parameters """ sample_input = "foo=100, bar=200" # First verify we can set both self.assertNotEquals(self.param_dict.get("foo"), 100) self.assertNotEquals(self.param_dict.get("bar"), 200) self.assertTrue(self.param_dict.update(sample_input)) self.assertEquals(self.param_dict.get("foo"), 100) self.assertEquals(self.param_dict.get("bar"), 200) # Now let's only have it update 1 parameter with a name sample_input = "foo=200, bar=300" self.assertTrue( self.param_dict.update(sample_input, target_params="foo")) self.assertEquals(self.param_dict.get("foo"), 200) self.assertEquals(self.param_dict.get("bar"), 200) # Now let's only have it update 1 parameter using a list sample_input = "foo=300, bar=400" self.assertTrue( self.param_dict.update(sample_input, target_params=["foo"])) self.assertEquals(self.param_dict.get("foo"), 300) self.assertEquals(self.param_dict.get("bar"), 200) # Test our exceptions with self.assertRaises(KeyError): self.param_dict.update(sample_input, "key_does_not_exist") with self.assertRaises(InstrumentParameterException): self.param_dict.update(sample_input, {'bad': "key_does_not_exist"}) def test_visibility_list(self): lst = self.param_dict.get_visibility_list( ParameterDictVisibility.READ_WRITE) lst.sort() self.assertEquals(lst, ["bar", "foo"]) lst = self.param_dict.get_visibility_list( ParameterDictVisibility.DIRECT_ACCESS) lst.sort() self.assertEquals(lst, ["baz", "qut"]) lst = self.param_dict.get_visibility_list( ParameterDictVisibility.READ_ONLY) lst.sort() self.assertEquals(lst, ["bat", "qux"]) lst = self.param_dict.get_visibility_list( ParameterDictVisibility.IMMUTABLE) lst.sort() self.assertEquals(lst, ["dil", "pho"]) def test_function_values(self): """ Make sure we can add and update values with functions instead of patterns """ self.param_dict.add_parameter( FunctionParameter("fn_foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE)) self.param_dict.add_parameter( FunctionParameter( "fn_bar", lambda x: bool(x & 2), # bit map example lambda x: str(x), direct_access=True, startup_param=True, value=False, visibility=ParameterDictVisibility.READ_WRITE)) # check defaults just to be safe val = self.param_dict.get("fn_foo") self.assertEqual(val, 1) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) self.param_dict.update(1005) # just change first in list val = self.param_dict.get("fn_foo") self.assertEqual(val, 3) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # fn_bar does not get updated here result = self.param_dict.update_many(1205) self.assertEqual(result['fn_foo'], True) self.assertEqual(len(result), 1) val = self.param_dict.get("fn_foo") self.assertEqual(val, 4) val = self.param_dict.get("fn_bar") self.assertEqual(val, False) # both are updated now result = self.param_dict.update_many(6) self.assertEqual(result['fn_foo'], True) self.assertEqual(result['fn_bar'], True) self.assertEqual(len(result), 2) val = self.param_dict.get("fn_foo") self.assertEqual(val, 0) val = self.param_dict.get("fn_bar") self.assertEqual(val, True) def test_mixed_pdv_types(self): """ Verify we can add different types of PDVs in one container """ self.param_dict.add_parameter( FunctionParameter("fn_foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1, visibility=ParameterDictVisibility.READ_WRITE)) self.param_dict.add_parameter( RegexParameter("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, value=10, visibility=ParameterDictVisibility.READ_WRITE)) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, value=15, visibility=ParameterDictVisibility.READ_WRITE) self.assertEqual(self.param_dict.get("fn_foo"), 1) self.assertEqual(self.param_dict.get("foo"), 10) self.assertEqual(self.param_dict.get("bar"), 15) def test_base_update(self): pdv = Parameter("foo", lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) result = pdv.update(1) self.assertEqual(result, True) self.assertEqual(pdv.get_value(), 1) # Its a base class...monkey see, monkey do result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.get_value(), "foo=1") def test_regex_val(self): pdv = RegexParameter("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) result = pdv.update(1) self.assertEqual(result, False) self.assertEqual(pdv.get_value(), 12) result = pdv.update("foo=1") self.assertEqual(result, True) self.assertEqual(pdv.get_value(), 1) def test_function_val(self): pdv = FunctionParameter("foo", self.pick_byte2, lambda x: str(x), value=12) self.assertEqual(pdv.get_value(), 12) self.assertRaises(TypeError, pdv.update(1)) result = pdv.update("1205") self.assertEqual(pdv.get_value(), 4) self.assertEqual(result, True) def test_set_init_value(self): result = self.param_dict.get("foo") self.assertEqual(result, None) self.param_dict.set_init_value("foo", 42) result = self.param_dict.get_init_value("foo") self.assertEqual(result, 42) def test_schema_generation(self): self.maxDiff = None result = self.param_dict.generate_dict() json_result = json.dumps(result, indent=4, sort_keys=True) log.debug("Expected: %s", self.target_schema) log.debug("Result: %s", json_result) self.assertEqual(result, self.target_schema) def test_empty_schema(self): self.param_dict = ProtocolParameterDict() result = self.param_dict.generate_dict() self.assertEqual(result, {}) def test_bad_descriptions(self): self.param_dict._param_dict["foo"].description = None self.param_dict._param_dict["foo"].value = None self.assertRaises(InstrumentParameterException, self.param_dict.get_init_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_default_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.set_default, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_init_value, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_menu_path_read, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_submenu_read, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_menu_path_write, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.get_submenu_write, "foo") self.assertRaises(InstrumentParameterException, self.param_dict.format, "foo", 1) self.assertRaises(InstrumentParameterException, self.param_dict.get_direct_access_list) self.assertRaises(InstrumentParameterException, self.param_dict.is_startup_param, "foo") def test_set(self): """ Test a simple set of the parameter. Make sure the right values get called and the correct exceptions are raised. """ new_param = FunctionParameter( "foo", self.pick_byte2, lambda x: str(x), direct_access=True, startup_param=True, value=1000, visibility=ParameterDictVisibility.READ_WRITE) self.assertEquals(new_param.get_value(), 1000) self.assertEquals(self.param_dict.get("foo"), None) # overwrites existing param self.param_dict.add_parameter(new_param) self.assertEquals(self.param_dict.get("foo"), 1000) self.param_dict.set_value("foo", 2000) self.assertEquals(self.param_dict.get("foo"), 2000) def test_invalid_type(self): self.assertRaises( InstrumentParameterException, FunctionParameter, "fn_bar", lambda x: bool(x & 2), # bit map example lambda x: str(x), direct_access=True, startup_param=True, value=False, type="bad_type", visibility=ParameterDictVisibility.READ_WRITE) def test_get(self): """ test getting values with expiration """ # from mi.core.exceptions import InstrumentParameterExpirationException pd = ProtocolParameterDict() # No expiration, should work just fine pd.add('noexp', r'', None, None, expiration=None) pd.add('zeroexp', r'', None, None, expiration=0) pd.add('lateexp', r'', None, None, expiration=2) ### # Set and get with no expire ### pd.set_value('noexp', 1) self.assertEqual(pd.get('noexp'), 1) ### # Set and get with a 0 expire ### basetime = pd.get_current_timestamp() pd.set_value('zeroexp', 2) # We should fail because we are calculating exp against current time with self.assertRaises(InstrumentParameterExpirationException): pd.get('zeroexp') # Should succeed because exp is calculated using basetime self.assertEqual(pd.get('zeroexp', basetime), 2) ### # Set and get with a delayed expire ### basetime = pd.get_current_timestamp() futuretime = pd.get_current_timestamp(3) self.assertGreater(futuretime - basetime, 3) pd.set_value('lateexp', 2) # Success because data is not expired self.assertEqual(pd.get('lateexp', basetime), 2) # Fail because data is expired (simulated three seconds from now) with self.assertRaises(InstrumentParameterExpirationException): pd.get('lateexp', futuretime) def test_regex_flags(self): pdv = RegexParameter("foo", r'.+foo=(\d+).+', lambda match: int(match.group(1)), lambda x: str(x), regex_flags=re.DOTALL, value=12) # Assert something good with dotall update() self.assertTrue(pdv) pdv.update("\n\nfoo=1212\n\n") self.assertEqual(pdv.get_value(), 1212) # negative test with no regex_flags pdv = RegexParameter("foo", r'.+foo=(\d+).+', lambda match: int(match.group(1)), lambda x: str(x), value=12) # Assert something good with dotall update() self.assertTrue(pdv) pdv.update("\n\nfoo=1212\n\n") self.assertEqual(pdv.get_value(), 12) self.assertRaises(TypeError, RegexParameter, "foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), regex_flags="bad flag", value=12) def test_format_current(self): self.param_dict.add("test_format", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: x + 5, value=10) self.assertEqual(self.param_dict.format("test_format", 20), 25) self.assertEqual(self.param_dict.format("test_format"), 15) self.assertRaises(KeyError, self.param_dict.format, "bad_name") def _assert_metadata_change(self): new_dict = self.param_dict.generate_dict() log.debug("Generated dictionary: %s", new_dict) self.assertEqual(new_dict["qut"][ParameterDictKey.DESCRIPTION], "QutFileDesc") self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutDisplay") self.assertEqual( new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.UNITS], "QutFileUnits") self.assertEqual( new_dict["qut"][ParameterDictKey.VALUE][ ParameterDictKey.DESCRIPTION], "QutFileValueDesc") self.assertEqual( new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.TYPE], "QutFileType") # Should come from hard code # self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutFileName") # from base hard code new_dict = self.param_dict.generate_dict() self.assertEqual(new_dict["baz"][ParameterDictKey.DESCRIPTION], "The baz parameter") self.assertEqual( new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.UNITS], "nano-bazers") self.assertEqual( new_dict["baz"][ParameterDictKey.VALUE][ ParameterDictKey.DESCRIPTION], "Should be an integer between 2 and 2000") self.assertEqual( new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.TYPE], ParameterDictType.INT) self.assertEqual(new_dict["baz"][ParameterDictKey.DISPLAY_NAME], "Baz") self.assertTrue('extra_param' not in new_dict)
def test_get(self): """ test getting values with expiration """ # from mi.core.exceptions import InstrumentParameterExpirationException pd = ProtocolParameterDict() # No expiration, should work just fine pd.add('noexp', r'', None, None, expiration=None) pd.add('zeroexp', r'', None, None, expiration=0) pd.add('lateexp', r'', None, None, expiration=2) ### # Set and get with no expire ### pd.set_value('noexp', 1) self.assertEqual(pd.get('noexp'), 1) ### # Set and get with a 0 expire ### basetime = pd.get_current_timestamp() pd.set_value('zeroexp', 2) # We should fail because we are calculating exp against current time with self.assertRaises(InstrumentParameterExpirationException): pd.get('zeroexp') # Should succeed because exp is calculated using basetime self.assertEqual(pd.get('zeroexp', basetime), 2) ### # Set and get with a delayed expire ### basetime = pd.get_current_timestamp() futuretime = pd.get_current_timestamp(3) self.assertGreater(futuretime - basetime, 3) pd.set_value('lateexp', 2) # Success because data is not expired self.assertEqual(pd.get('lateexp', basetime), 2) # Fail because data is expired (simulated three seconds from now) with self.assertRaises(InstrumentParameterExpirationException): pd.get('lateexp', futuretime)
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add( Parameter.FLUSH_VOLUME, r"Flush Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Flush Volume", description="Amount of sea water to flush prior to taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FLUSH_FLOWRATE, r"Flush Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Flush Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FLUSH_MINFLOW, r"Flush Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Flush Minimum Flow", description="If the flow rate falls below this value the flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_VOLUME, r"Fill Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Fill Volume", description="Amount of seawater to run through the collection filter (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_FLOWRATE, r"Fill Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Fill Flow Rate", description="Flow rate during sampling: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.FILL_MINFLOW, r"Fill Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Fill Minimum Flow", description="If the flow rate falls below this value the fill will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_VOLUME, r"Reverse Volume: (.*)mL", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Clear Volume", description="Amount of sea water to flush the home port after taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_FLOWRATE, r"Reverse Flow Rate: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Clear Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.add( Parameter.CLEAR_MINFLOW, r"Reverse Min Flow: (.*)mL/min", None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units=Prefixes.MILLI + Units.LITER + "/" + Units.MINUTE, startup_param=True, display_name="Clear Minimum Flow", description="If the flow rate falls below this value the reverse flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE, ) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. handlers = { ProtocolState.UNKNOWN: [ (ProtocolEvent.ENTER, self._handler_unknown_enter), (ProtocolEvent.EXIT, self._handler_unknown_exit), (ProtocolEvent.DISCOVER, self._handler_unknown_discover), ], ProtocolState.COMMAND: [ (ProtocolEvent.ENTER, self._handler_command_enter), (ProtocolEvent.EXIT, self._handler_command_exit), (ProtocolEvent.START_DIRECT, self._handler_command_start_direct), (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_sample), (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_autosample), (ProtocolEvent.GET, self._handler_get), (ProtocolEvent.SET, self._handler_command_set), ], ProtocolState.AUTOSAMPLE: [ (ProtocolEvent.ENTER, self._handler_autosample_enter), (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_sample), (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop), (ProtocolEvent.EXIT, self._handler_autosample_exit), ], ProtocolState.DIRECT_ACCESS: [ (ProtocolEvent.ENTER, self._handler_direct_access_enter), (ProtocolEvent.EXIT, self._handler_direct_access_exit), (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct), (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct), ], } for state in handlers: for event, handler in handlers[state]: self._protocol_fsm.add_handler(state, event, handler) # Add build handlers for device commands - we are only using simple commands for cmd in Command.list(): self._add_build_handler(cmd, self._build_command) self._add_response_handler(cmd, self._check_command) self._add_build_handler(Command.SETUP, self._build_setup_command) self._add_response_handler(Command.READ_SETUP, self._read_setup_response_handler) # Add response handlers for device commands. # self._add_response_handler(Command.xyz, self._parse_xyz_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) self._sent_cmds = None self.initialize_scheduler() # unit identifiers - must match the setup command (SU31 - '1') self._units = ['1', '2', '3'] self._setup = None # set by the read setup command handler for comparison to see if the config needs reset @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(D1000TemperatureDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) if not return_list: log.debug("sieve_function: raw_data=%r, return_list=%s", raw_data, return_list) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s", chunk) self._extract_sample(D1000TemperatureDataParticle, D1000TemperatureDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # implement virtual methods from base class. ######################################################################## def _set_params(self, *args, **kwargs): """ Issue commands to the instrument to set various parameters. If startup is set to true that means we are setting startup values and immutable parameters can be set. Otherwise only READ_WRITE parameters can be set. must be overloaded in derived classes @param params dictionary containing parameter name and value pairs @param startup - a flag, true indicates initializing, false otherwise """ params = args[0] # check for attempt to set readonly parameters (read-only or immutable set outside startup) self._verify_not_readonly(*args, **kwargs) old_config = self._param_dict.get_config() for (key, val) in params.iteritems(): log.debug("KEY = " + str(key) + " VALUE = " + str(val)) self._param_dict.set_value(key, val) new_config = self._param_dict.get_config() # check for parameter change if not dict_equal(old_config, new_config): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def apply_startup_params(self): """ Apply startup parameters """ config = self.get_startup_config() for param in Parameter.list(): if param in config: self._param_dict.set_value(param, config[param]) ######################################################################## # Private helpers. ######################################################################## def _wakeup(self, wakeup_timeout=10, response_timeout=3): """ Over-ridden because the D1000 does not go to sleep and requires no special wake-up commands. @param wakeup_timeout The timeout to wake the device. @param response_timeout The time to look for response to a wakeup attempt. @throw InstrumentTimeoutException if the device could not be woken. """ pass def _do_command(self, cmd, unit, **kwargs): """ Send command and ensure it matches appropriate response. Simply enforces sending the unit identifier as a required argument. @param cmd - Command to send to instrument @param unit - unit identifier @retval - response from instrument """ self._do_cmd_resp(cmd, unit, write_delay=INTER_CHARACTER_DELAY, **kwargs) def _build_command(self, cmd, unit): """ @param cmd - Command to process @param unit - unit identifier """ return '#' + unit + cmd + NEWLINE def _build_setup_command(self, cmd, unit): """ @param cmd - command to send - should be 'SU' @param unit - unit identifier - should be '1', '2', or '3', must be a single character """ # use defaults - in the future, may consider making some of these parameters # byte 0 channel_address = unit # byte 1 line_feed = self._param_dict.format(Parameter.LINEFEED) parity_type = self._param_dict.format(Parameter.PARITY_TYPE) parity_enable = self._param_dict.format(Parameter.PARITY_ENABLE) extended_addressing = self._param_dict.format(Parameter.EXTENDED_ADDRESSING) baud_rate = self._param_dict.format(Parameter.BAUD_RATE) baud_rate = getattr(BaudRate, 'BAUD_%d' % baud_rate, BaudRate.BAUD_9600) # byte 2 alarm_enable = self._param_dict.format(Parameter.ALARM_ENABLE) low_alarm_latch = self._param_dict.format(Parameter.LOW_ALARM_LATCH) high_alarm_latch = self._param_dict.format(Parameter.HIGH_ALARM_LATCH) rtd_wire = self._param_dict.format(Parameter.RTD_4_WIRE) temp_units = self._param_dict.format(Parameter.TEMP_UNITS) echo = self._param_dict.format(Parameter.ECHO) delay_units = self._param_dict.format(Parameter.COMMUNICATION_DELAY) # byte 3 precision = self._param_dict.format(Parameter.PRECISION) precision = getattr(UnitPrecision, 'DIGITS_%d' % precision, UnitPrecision.DIGITS_6) large_signal_filter_constant = self._param_dict.format(Parameter.LARGE_SIGNAL_FILTER_C) large_signal_filter_constant = filter_enum(large_signal_filter_constant) small_signal_filter_constant = self._param_dict.format(Parameter.SMALL_SIGNAL_FILTER_C) small_signal_filter_constant = filter_enum(small_signal_filter_constant) # # Factory default: 0x31070182 # # Lab default: 0x310214C2 byte_0 = int(channel_address.encode("hex"), 16) log.debug('byte 0: %s', byte_0) byte_1 = \ (line_feed << 7) + \ (parity_type << 6) + \ (parity_enable << 5) + \ (extended_addressing << 4) + \ baud_rate log.debug('byte 1: %s', byte_1) byte_2 = \ (alarm_enable << 7) + \ (low_alarm_latch << 6) + \ (high_alarm_latch << 5) + \ (rtd_wire << 4) + \ (temp_units << 3) + \ (echo << 2) + \ delay_units log.debug('byte 2: %s', byte_2) byte_3 = \ (precision << 6) + \ (large_signal_filter_constant << 3) + \ small_signal_filter_constant log.debug('byte 3: %s', byte_3) setup_command = '#%sSU%02x%02x%02x%02x' % (unit[0], byte_0, byte_1, byte_2, byte_3) + NEWLINE log.debug('default setup command (%r) for unit %02x (%s)' % (setup_command, byte_0, unit[0])) return setup_command def _check_command(self, resp, prompt): """ Perform a checksum calculation on provided data. The checksum used for comparison is the last two characters of the line. @param resp - response from the instrument to the command @param prompt - expected prompt (or the joined groups from a regex match) @retval """ for line in resp.split(NEWLINE): if line.startswith('?'): raise InstrumentProtocolException('error processing command (%r)', resp[1:]) if line.startswith('*'): # response if not valid_response(line): raise InstrumentProtocolException('checksum failed (%r)', line) def _read_setup_response_handler(self, resp, prompt): """ Save the setup. @param resp - response from the instrument to the command @param prompt - expected prompt (or the joined groups from a regex match) """ self._check_command(resp, prompt) self._setup = resp def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with commands. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample", timeout=40) self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="Acquire Sample") self._cmd_dict.add(Capability.DISCOVER, display_name='Discover', timeout=30) def _add_setup_param(self, name, fmt, **kwargs): """ Add setup command to the parameter dictionary. All 'SU' parameters are not startup parameter, but should be restored upon return from direct access. These parameters are all part of the instrument command 'SU'. """ self._param_dict.add(name, '', None, fmt, startup_param=False, direct_access=True, visibility=ParameterDictVisibility.READ_ONLY, **kwargs) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, startup_param=True, display_name='D1000 Sample Periodicity', range=(1, 3600), description='Periodicity of D1000 temperature sample in autosample mode: (1-3600)', default_value=DEFAULT_SAMPLE_RATE, units=Units.SECOND, visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, type=ParameterDictType.INT, display_name='Base Channel Address', description='Hex value of ASCII character to ID unit, e.g. 31 is the ASCII code for 1:' ' (30-31, 41-5A, 61-7A)', range=(0x30, 0x7A), default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, type=ParameterDictType.BOOL, display_name='Line Feed Flag', range={'True': True, 'False': False}, description='Enable D1000 to generate a linefeed before and after each response:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, type=ParameterDictType.BOOL, display_name='Parity Type', range={'Odd': True, 'Even': False}, description='Sets the parity: (true:odd | false:even)', default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Parity Flag', range={'True': True, 'False': False}, description='Enable use of parity bit, a parity error will be issued if detected:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, type=ParameterDictType.BOOL, display_name='Extended Addressing', range={'True': True, 'False': False}, description='Enable extended addressing: (true | false)', default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, type=ParameterDictType.INT, display_name='Baud Rate', range={'38400': 0, '19200': 1, '9600': 2, '4800': 3, '2400': 4, '1200': 5, '600': 6, '300': 7, '57600': 8}, description='Using ethernet interface in deployed configuration: (300, 600, ' '1200, 2400, 4800, 9600, 19200, 38400, 57600)', default_value=9600, units=Units.BAUD) self._add_setup_param(Parameter.ALARM_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Enable Alarms', range={'True': True, 'False': False}, description='Enable alarms to be controlled by the Digital Output (DO) command:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='Low Alarm Latching', range={'True': True, 'False': False}, description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='High Alarm Latching', range={'True': True, 'False': False}, description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, type=ParameterDictType.BOOL, display_name='4 Wire RTD Flag', range={'True': True, 'False': False}, description='Represents a physical configuration of the instrument, ' 'disabling may cause data to be misaligned: (true | false)', default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, type=ParameterDictType.BOOL, display_name='Fahrenheit Flag', range={'Fahrenheit': True, 'Celsius': False}, description='Flag to control the temperature format: (true:Fahrenheit | false:Celsius)', default_value=False) self._add_setup_param(Parameter.ECHO, bool, type=ParameterDictType.BOOL, display_name='Daisy Chain', range={'True': True, 'False': False}, description='If not set, only 1 out of 3 D1000s will process commands: (true | false)', default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, type=ParameterDictType.INT, display_name='Communication Delay', range=(0, 3), description='The number of delays to add when processing commands: (0-3)', default_value=0) self._add_setup_param(Parameter.PRECISION, int, type=ParameterDictType.INT, display_name='Precision', range={'4 digits': 0, '5 digits': 1, '6 digits': 2, '7 digits': 3}, description='Number of digits the instrument should output for temperature query: ' '(0=4-3=7)', default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Large Signal Filter Constant', range={'0': 0, '.25': 1, '.5': 2, '1': 3, '2': 4, '4': 5, '8': 6, '16': 7}, description='Time to reach 63% of its final value: ' '(0 = 0.0, 1 = 0.25, 2 = 0.5, 3 = 1.0, 4 = 2.0, 5 = 4.0, 6 = 8.0, 7 = 16.0)', default_value=0.0, units=Units.SECOND) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Small Signal Filter Constant', range={'0': 0, '.25': 1, '.5': 2, '1': 3, '2': 4, '4': 5, '8': 6, '16': 7}, description='Smaller filter constant, should be larger than large filter constant: ' '(0 = 0.0, 1 = 0.25, 2 = 0.5, 3 = 1.0, 4 = 2.0, 5 = 4.0, 6 = 8.0, 7 = 16.0)', default_value=0.50, units=Units.SECOND) for key in self._param_dict.get_keys(): self._param_dict.set_default(key) def _update_params(self): """ Update the parameter dictionary. """ pass def _restore_params(self): """ Restore D1000, clearing any alarms and set-point. """ # make sure the alarms are disabled - preferred over doing setup, then clear alarms commands self._param_dict.set_value(Parameter.ALARM_ENABLE, False) for i in self._units: current_setup = None # set in READ_SETUP response handler try: self._do_command(Command.READ_SETUP, i, response_regex=Response.READ_SETUP) current_setup = self._setup[4:][:-2] # strip off the leader and checksum except InstrumentTimeoutException: log.error('D1000 unit %s has been readdressed, unable to restore settings' % i[0]) new_setup = self._build_setup_command(Command.SETUP, i)[4:] # strip leader (no checksum) if not current_setup == new_setup: log.debug('restoring setup to default state (%s) from current state (%s)', new_setup, current_setup) self._do_command(Command.ENABLE_WRITE, i) self._do_command(Command.SETUP, i) self._do_command(Command.ENABLE_WRITE, i) self._do_command(Command.CLEAR_ZERO, i) ######################################################################## # Event handlers for UNKNOWN state. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state @retval next_state, (next_state, result) """ # force to command mode, this instrument has no autosample mode next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result) ######################################################################## # Event handlers for COMMAND state. ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ # Command device to update parameters and send a config change event if needed. self._restore_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ input_params = args[0] for key, value in input_params.items(): if not Parameter.has(key): raise InstrumentParameterException('Invalid parameter supplied to set: %s' % key) try: value = int(value) except TypeError: raise InstrumentParameterException('Invalid value [%s] for parameter %s' % (value, key)) if key == Parameter.SAMPLE_INTERVAL: if value < MIN_SAMPLE_RATE or value > MAX_SAMPLE_RATE: raise InstrumentParameterException('Parameter %s value [%d] is out of range [%d %d]' % (key, value, MIN_SAMPLE_RATE, MAX_SAMPLE_RATE)) startup = False try: startup = args[1] except IndexError: pass self._set_params(input_params, startup) return None, None def _handler_command_autosample(self, *args, **kwargs): """ Begin autosample. """ return ProtocolState.AUTOSAMPLE, (ProtocolState.AUTOSAMPLE, None) def _handler_command_start_direct(self, *args, **kwargs): next_state = ProtocolState.DIRECT_ACCESS result = [] return next_state, (next_state, result) ######################################################################## # Event handlers for AUTOSAMPLE state. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Start auto polling the temperature sensors. """ self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._protocol_fsm.on_event(ProtocolEvent.ACQUIRE_SAMPLE) job_name = ScheduledJob.SAMPLE config = { DriverConfigKey.SCHEDULER: { job_name: { DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.SAMPLE_INTERVAL) } } } } self.set_init_params(config) self._add_scheduler_event(ScheduledJob.SAMPLE, ProtocolEvent.ACQUIRE_SAMPLE) def _handler_autosample_exit(self, *args, **kwargs): """ Stop autosampling - remove the scheduled autosample """ if self._scheduler is not None: try: self._remove_scheduler(ScheduledJob.SAMPLE) except KeyError: log.debug('_remove_scheduler count not find: %s', ScheduledJob.SAMPLE) def _handler_sample(self, *args, **kwargs): """ Poll the three temperature probes for current temperature readings. """ next_state = None timeout = time.time() + SAMPLE_TIMEOUT for i in self._units: self._do_command(Command.READ, i) particles = self.wait_for_particles([DataParticleType.D1000_PARSED], timeout) return next_state, (next_state, particles) def _handler_autosample_stop(self, *args, **kwargs): """ Terminate autosampling """ next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ def _handler_direct_access_execute_direct(self, data): self._do_cmd_direct(data) return None, (None, []) def _handler_direct_access_stop_direct(self, *args, **kwargs): next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result)
class Protocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ last_sample = '' def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit) self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_SAMPLE, self._handler_acquire_sample) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.CLOCK_SYNC, self._handler_command_sync_clock) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.FLASH_STATUS, self._handler_flash_status) self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ENTER, self._handler_autosample_enter) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.EXIT, self._handler_autosample_exit) self._protocol_fsm.add_handler( ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ACQUIRE_SAMPLE, self._handler_acquire_sample) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.CLOCK_SYNC, self._handler_autosample_sync_clock) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.GET, self._handler_get) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.FLASH_STATUS, self._handler_flash_status) self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status) # We setup a new state for clock sync because then we could use the state machine so the autosample scheduler # is disabled before we try to sync the clock. Otherwise there could be a race condition introduced when we # are syncing the clock and the scheduler requests a sample. self._protocol_fsm.add_handler(ProtocolState.SYNC_CLOCK, ProtocolEvent.ENTER, self._handler_sync_clock_enter) self._protocol_fsm.add_handler(ProtocolState.SYNC_CLOCK, ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock_sync) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit) self._protocol_fsm.add_handler( ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct) self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct) # Add build handlers for device commands. self._add_build_handler(Command.GET_CLOCK, self._build_simple_command) self._add_build_handler(Command.SET_CLOCK, self._build_set_clock_command) self._add_build_handler(Command.D, self._build_simple_command) self._add_build_handler(Command.GO, self._build_simple_command) self._add_build_handler(Command.STOP, self._build_simple_command) self._add_build_handler(Command.FS, self._build_simple_command) self._add_build_handler(Command.STAT, self._build_simple_command) # Add response handlers for device commands. self._add_response_handler(Command.GET_CLOCK, self._parse_clock_response) self._add_response_handler(Command.SET_CLOCK, self._parse_clock_response) self._add_response_handler(Command.FS, self._parse_fs_response) self._add_response_handler(Command.STAT, self._parse_common_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(Protocol.sieve_function) self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, ProtocolEvent.ACQUIRE_STATUS) self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) @staticmethod def sieve_function(raw_data): """ The method that splits samples and status """ matchers = [] return_list = [] matchers.append(METBK_SampleDataParticle.regex_compiled()) matchers.append(METBK_StatusDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ log.debug("_got_chunk: chunk=%s" % chunk) self._extract_sample(METBK_SampleDataParticle, METBK_SampleDataParticle.regex_compiled(), chunk, timestamp) self._extract_sample(METBK_StatusDataParticle, METBK_StatusDataParticle.regex_compiled(), chunk, timestamp) def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # override methods from base class. ######################################################################## def _extract_sample(self, particle_class, regex, line, timestamp, publish=True): """ Overridden to add duplicate sample checking. This duplicate checking should only be performed on sample chunks and not other chunk types, therefore the regex is performed before the string checking. Extract sample from a response line if present and publish parsed particle @param particle_class The class to instantiate for this specific data particle. Parameterizing this allows for simple, standard behavior from this routine @param regex The regular expression that matches a data sample @param line string to match for sample. @param timestamp port agent timestamp to include with the particle @param publish boolean to publish samples (default True). If True, two different events are published: one to notify raw data and the other to notify parsed data. @retval dict of dicts {'parsed': parsed_sample, 'raw': raw_sample} if the line can be parsed for a sample. Otherwise, None. @todo Figure out how the agent wants the results for a single poll and return them that way from here """ sample = None match = regex.match(line) if match: if particle_class == METBK_SampleDataParticle: # check to see if there is a delta from last sample, and don't parse this sample if there isn't if match.group(0) == self.last_sample: return # save this sample as last_sample for next check self.last_sample = match.group(0) particle = particle_class(line, port_timestamp=timestamp) parsed_sample = particle.generate() if publish and self._driver_event: self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample) sample = json.loads(parsed_sample) return sample return sample ######################################################################## # implement virtual methods from base class. ######################################################################## def apply_startup_params(self): """ Apply sample_interval startup parameter. """ config = self.get_startup_config() log.debug("apply_startup_params: startup config = %s" % config) if config.has_key(Parameter.SAMPLE_INTERVAL): log.debug("apply_startup_params: setting sample_interval to %d" % config[Parameter.SAMPLE_INTERVAL]) self._param_dict.set_value(Parameter.SAMPLE_INTERVAL, config[Parameter.SAMPLE_INTERVAL]) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unknown_exit(self, *args, **kwargs): """ Exit unknown state. """ pass def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ (protocol_state, agent_state) = self._discover() # If we are just starting up and we land in command mode then our state should # be idle if (agent_state == ResourceAgentState.COMMAND): agent_state = ResourceAgentState.IDLE log.debug("_handler_unknown_discover: state = %s", protocol_state) return (protocol_state, agent_state) ######################################################################## # Clock Sync handlers. # Not much to do in this state except sync the clock then transition # back to autosample. When in command mode we don't have to worry about # stopping the scheduler so we just sync the clock without state # transitions ######################################################################## def _handler_sync_clock_enter(self, *args, **kwargs): """ Enter sync clock state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._protocol_fsm.on_event(ProtocolEvent.CLOCK_SYNC) def _handler_sync_clock_sync(self, *args, **kwargs): """ Sync the clock """ next_state = ProtocolState.AUTOSAMPLE next_agent_state = ResourceAgentState.STREAMING result = None self._sync_clock() self._async_agent_state_change(ResourceAgentState.STREAMING) return (next_state, (next_agent_state, result)) ######################################################################## # Command handlers. # just implemented to make DA possible, instrument has no actual command mode ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ self._init_params() # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_exit(self, *args, **kwargs): """ Exit command state. """ pass def _handler_command_set(self, *args, **kwargs): """ no writable parameters so does nothing, just implemented to make framework happy """ next_state = None result = None return (next_state, result) def _handler_command_start_direct(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.DIRECT_ACCESS next_agent_state = ResourceAgentState.DIRECT_ACCESS return (next_state, (next_agent_state, result)) def _handler_command_start_autosample(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.AUTOSAMPLE next_agent_state = ResourceAgentState.STREAMING self._start_logging() return (next_state, (next_agent_state, result)) def _handler_command_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None self._sync_clock() return (next_state, (next_agent_state, result)) ######################################################################## # autosample handlers. ######################################################################## def _handler_autosample_enter(self, *args, **kwargs): """ Enter autosample state Because this is an instrument that must be polled we need to ensure the scheduler is added when we are in an autosample state. This scheduler raises events to poll the instrument for data. """ self._init_params() self._ensure_autosample_config() self._add_scheduler_event(AUTO_SAMPLE_SCHEDULED_JOB, ProtocolEvent.ACQUIRE_SAMPLE) # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_autosample_exit(self, *args, **kwargs): """ exit autosample state. """ self._remove_scheduler(AUTO_SAMPLE_SCHEDULED_JOB) def _handler_autosample_stop_autosample(self, *args, **kwargs): """ """ result = None next_state = ProtocolState.COMMAND next_agent_state = ResourceAgentState.COMMAND self._stop_logging() return (next_state, (next_agent_state, result)) def _handler_autosample_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = ProtocolState.SYNC_CLOCK next_agent_state = ResourceAgentState.BUSY result = None return (next_state, (next_agent_state, result)) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_exit(self, *args, **kwargs): """ Exit direct access state. """ pass def _handler_direct_access_execute_direct(self, data): next_state = None result = None self._do_cmd_direct(data) return (next_state, result) def _handler_direct_access_stop_direct(self): result = None (next_state, next_agent_state) = self._discover() return (next_state, (next_agent_state, result)) ######################################################################## # general handlers. ######################################################################## def _handler_flash_status(self, *args, **kwargs): """ Acquire flash status from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None result = self._do_cmd_resp(Command.FS, expected_prompt=Prompt.FS) log.debug("FLASH RESULT: %s", result) return (next_state, (next_agent_state, result)) def _handler_acquire_sample(self, *args, **kwargs): """ Acquire sample from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None result = self._do_cmd_resp(Command.D, *args, **kwargs) return (next_state, (next_agent_state, result)) def _handler_acquire_status(self, *args, **kwargs): """ Acquire status from instrument. @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device cannot be woken for command. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None log.debug("Logging status: %s", self._is_logging()) result = self._do_cmd_resp(Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.GO]) return (next_state, (next_agent_state, result)) ######################################################################## # Private helpers. ######################################################################## def _set_params(self, *args, **kwargs): """ Overloaded from the base class, used in apply DA params. Not needed here so just noop it. """ pass def _discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode). @retval (next_state, result), (ProtocolState.COMMAND, None) if successful. """ logging = self._is_logging() if (logging == True): protocol_state = ProtocolState.AUTOSAMPLE agent_state = ResourceAgentState.STREAMING elif (logging == False): protocol_state = ProtocolState.COMMAND agent_state = ResourceAgentState.COMMAND else: protocol_state = ProtocolState.UNKNOWN agent_state = ResourceAgentState.ACTIVE_UNKNOWN return (protocol_state, agent_state) def _start_logging(self): """ start the instrument logging if is isn't running already. """ if (not self._is_logging()): log.debug("Sending start logging command: %s", Command.GO) self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO) def _stop_logging(self): """ stop the instrument logging if is is running. When the instrument is in a syncing state we can not stop logging. We must wait before we sent the stop command. """ if (self._is_logging()): log.debug("Attempting to stop the instrument logging.") result = self._do_cmd_resp( Command.STOP, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) log.debug("Stop Command Result: %s", result) # If we are still logging then let's wait until we are not # syncing before resending the command. if (self._is_logging()): self._wait_for_sync() log.debug("Attempting to stop the instrument again.") result = self._do_cmd_resp( Command.STOP, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) log.debug("Stop Command Result: %s", result) def _wait_for_sync(self): """ When the instrument is syncing internal parameters we can't stop logging. So we will watch the logging status and when it is not synchronizing we will return. Basically we will just block until we are no longer syncing. @raise InstrumentProtocolException when we timeout waiting for a transition. """ timeout = time.time() + SYNC_TIMEOUT while (time.time() < timeout): result = self._do_cmd_resp( Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.SYNC, Prompt.GO]) match = LOGGING_SYNC_COMPILED.match(result) if (match): log.debug("We are still in sync mode. Wait a bit and retry") time.sleep(2) else: log.debug("Transitioned out of sync.") return True # We timed out raise InstrumentProtocolException( "failed to transition out of sync mode") def _is_logging(self): """ Run the status command to determine if we are in command or autosample mode. @return: True if sampling, false if not, None if we can't determine """ log.debug("_is_logging: start") result = self._do_cmd_resp(Command.STAT, expected_prompt=[Prompt.STOPPED, Prompt.GO]) log.debug("Checking logging status from %s", result) match = LOGGING_STATUS_COMPILED.match(result) if not match: log.error("Unable to determine logging status from: %s", result) return None if match.group(1) == 'GO': log.debug("Looks like we are logging: %s", match.group(1)) return True else: log.debug("Looks like we are NOT logging: %s", match.group(1)) return False def _ensure_autosample_config(self): scheduler_config = self._get_scheduler_config() if (scheduler_config == None): log.debug( "_ensure_autosample_config: adding scheduler element to _startup_config" ) self._startup_config[DriverConfigKey.SCHEDULER] = {} scheduler_config = self._get_scheduler_config() log.debug( "_ensure_autosample_config: adding autosample config to _startup_config" ) config = { DriverSchedulerConfigKey.TRIGGER: { DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL, DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.SAMPLE_INTERVAL) } } self._startup_config[ DriverConfigKey.SCHEDULER][AUTO_SAMPLE_SCHEDULED_JOB] = config if (not self._scheduler): self.initialize_scheduler() def _sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ next_state = None next_agent_state = None result = None time_format = "%Y/%m/%d %H:%M:%S" str_val = get_timestamp_delayed(time_format) log.debug("Setting instrument clock to '%s'", str_val) self._do_cmd_resp(Command.SET_CLOCK, str_val, expected_prompt=Prompt.CR_NL) def _wakeup(self, timeout): """There is no wakeup sequence for this instrument""" pass def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="start autosample") self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="stop autosample") self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="synchronize clock") self._cmd_dict.add(Capability.ACQUIRE_STATUS, display_name="acquire status") self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="acquire sample") self._cmd_dict.add(Capability.FLASH_STATUS, display_name="flash status") def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.CLOCK, r'(.*)\r\n', lambda match: match.group(1), lambda string: str(string), type=ParameterDictType.STRING, display_name="clock", expiration=0, visibility=ParameterDictVisibility.READ_ONLY) self._param_dict.add( Parameter.SAMPLE_INTERVAL, r'Not used. This parameter is not parsed from instrument response', None, self._int_to_string, type=ParameterDictType.INT, default_value=30, value=30, startup_param=True, display_name="sample_interval", visibility=ParameterDictVisibility.IMMUTABLE) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ log.debug("_update_params:") # Issue clock command and parse results. # This is the only parameter and it is always changing so don't bother with the 'change' event self._do_cmd_resp(Command.GET_CLOCK) def _build_set_clock_command(self, cmd, val): """ Build handler for set clock command (cmd=val followed by newline). @param cmd the string for setting the clock (this should equal #CLOCK=). @param val the parameter value to set. @ retval The set command to be sent to the device. """ cmd = '%s%s' % (cmd, val) + NEWLINE return cmd def _parse_clock_response(self, response, prompt): """ Parse handler for clock command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if clock command misunderstood. """ log.debug("_parse_clock_response: response=%s, prompt=%s" % (response, prompt)) if prompt not in [Prompt.CR_NL]: raise InstrumentProtocolException( 'CLOCK command not recognized: %s.' % response) if not self._param_dict.update(response): raise InstrumentProtocolException('CLOCK command not parsed: %s.' % response) return def _parse_fs_response(self, response, prompt): """ Parse handler for FS command. @param response command response string. @param prompt prompt following command response. @throws InstrumentProtocolException if FS command misunderstood. """ log.debug("_parse_fs_response: response=%s, prompt=%s" % (response, prompt)) if prompt not in [Prompt.FS]: raise InstrumentProtocolException( 'FS command not recognized: %s.' % response) return response def _parse_common_response(self, response, prompt): """ Parse handler for common commands. @param response command response string. @param prompt prompt following command response. """ return response
def _build_param_dict(self): """ Populate the parameter dictionary with parameters. For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() self._param_dict.add(Parameter.CYCLE_TIME, r'(\d+)\s+= Cycle Time \(.*\)\r\n(0|1)\s+= Minutes or Seconds Cycle Time', lambda match : self._to_seconds(int(match.group(1)), int(match.group(2))), self._int_to_string, visibility=ParameterDictVisibility.READ_WRITE, startup_param=True, direct_access=False, default_value=20, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["1", Prompt.CYCLE_TIME_PROMPT]]) self._param_dict.add(Parameter.VERBOSE, r'', # Write-only, so does it really matter? lambda match : None, self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=1, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["2", Prompt.VERBOSE_PROMPT]]) self._param_dict.add(Parameter.METADATA_POWERUP, r'(0|1)\s+= Metadata Print Status on Power up', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=0, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["3", Prompt.METADATA_PROMPT]]) self._param_dict.add(Parameter.METADATA_RESTART, r'(0|1)\s+= Metadata Print Status on Restart Data Collection', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=True, init_value=0, menu_path_write=SubMenu.CHANGE_PARAM, submenu_write=[["4", Prompt.METADATA_PROMPT]]) self._param_dict.add(Parameter.RES_SENSOR_POWER, r'(0|1)\s+= Res Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["1"]]) self._param_dict.add(Parameter.INST_AMP_POWER, r'(0|1)\s+= Thermocouple & Hydrogen Amp Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["2"]]) self._param_dict.add(Parameter.EH_ISOLATION_AMP_POWER, r'(0|1)\s+= eh Amp Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["3"]]) self._param_dict.add(Parameter.HYDROGEN_POWER, r'(0|1)\s+= Hydrogen Sensor Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["4"]]) self._param_dict.add(Parameter.REFERENCE_TEMP_POWER, r'(0|1)\s+= Reference Temperature Power Status', lambda match : int(match.group(1)), self._int_to_string, visibility=ParameterDictVisibility.READ_ONLY, startup_param=True, direct_access=False, init_value=1, menu_path_read=SubMenu.SHOW_PARAM, submenu_read=[], menu_path_write=SubMenu.SENSOR_POWER, submenu_write=[["5"]])
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, startup_param=True, display_name='D1000 Sample Periodicity', description='Periodicity of D1000 temperature sample in autosample mode: (1-3600)', default_value=DEFAULT_SAMPLE_RATE, units=Units.SECOND, visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, type=ParameterDictType.INT, display_name='Base Channel Address', description='Hex value of ASCII character to ID unit, e.g. 31 is the ASCII code for 1: (30-31, 41-5A, 61-7A)', default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, type=ParameterDictType.BOOL, display_name='Line Feed Flag', description='Enable D1000 to generate a linefeed before and after each response: (true | false)', default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, type=ParameterDictType.BOOL, display_name='Parity Type', description='Sets the parity: (true:odd | false:even)', default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Parity Flag', description='Enable use of parity bit, a parity error will be issued if detected: (true | false)', default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, type=ParameterDictType.BOOL, display_name='Extended Addressing', description='Enable extended addressing: (true | false)', default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, type=ParameterDictType.INT, display_name='Baud Rate', description='Using ethernet interface in deployed configuration: (300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600)', default_value=9600, units=Units.BAUD) self._add_setup_param(Parameter.ALARM_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Enable Alarms', description='Enable alarms to be controlled by the Digital Output (DO) command: (true | false)', default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='Low Alarm Latching', description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='High Alarm Latching', description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, type=ParameterDictType.BOOL, display_name='4 Wire RTD Flag', description='Represents a physical configuration of the instrument, disabling may cause data to be misaligned: (true | false)', default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, type=ParameterDictType.BOOL, display_name='Fahrenheit Flag', description='Flag to control the temperature format: (true:Fahrenheit | false:Celsius)', default_value=False) self._add_setup_param(Parameter.ECHO, bool, type=ParameterDictType.BOOL, display_name='Daisy Chain', description='If not set, only 1 out of 3 D1000s will process commands: (true | false)', default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, type=ParameterDictType.INT, display_name='Communication Delay', description='The number of delays to add when processing commands: (0-3)', default_value=0) self._add_setup_param(Parameter.PRECISION, int, type=ParameterDictType.INT, display_name='Precision', description='Number of digits the instrument should output for temperature query: (4-7)', default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Large Signal Filter Constant', description='Time to reach 63% of its final value: (0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0)', default_value=0.0, units=Units.SECOND) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Small Signal Filter Constant', description='Smaller filter constant, should be larger than large filter constant: (0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0)', default_value=0.50, units=Units.SECOND) for key in self._param_dict.get_keys(): self._param_dict.set_default(key)
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, default_value=DEFAULT_SAMPLE_RATE, startup_param=True, display_name='D1000 sample periodicity (sec)', visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, display_name='base channel address', type=ParameterDictType.INT, default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, display_name='line feed flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, display_name='parity type', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, display_name='parity flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, display_name='extended addressing', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, display_name='baud rate', type=ParameterDictType.INT, default_value=9600) self._add_setup_param(Parameter.ALARM_ENABLE, bool, display_name='enable alarms', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, display_name='low alarm latching', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, display_name='high alarm latching', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, display_name='4 wire RTD flag', type=ParameterDictType.BOOL, default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, display_name='Fahrenheit flag', type=ParameterDictType.BOOL, default_value=False) self._add_setup_param(Parameter.ECHO, bool, display_name='daisy chain', type=ParameterDictType.BOOL, default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, display_name='communication delay', type=ParameterDictType.INT, default_value=0) self._add_setup_param(Parameter.PRECISION, int, display_name='precision', type=ParameterDictType.INT, default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, display_name='large signal filter constant', type=ParameterDictType.FLOAT, default_value=0.0) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, display_name='small signal filter constant', type=ParameterDictType.FLOAT, default_value=0.50) for key in self._param_dict.get_keys(): self._param_dict.set_default(key)
def setUp(self): self.param_dict = ProtocolParameterDict() self.param_dict.add("foo", r'.*foo=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, startup_param=True, default_value=10, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add("bar", r'.*bar=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=False, startup_param=True, default_value=15, visibility=ParameterDictVisibility.READ_WRITE) self.param_dict.add( "baz", r'.*baz=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=20, visibility=ParameterDictVisibility.DIRECT_ACCESS, get_timeout=30, set_timeout=40, display_name="Baz", description="The baz parameter", type=ParameterDictType.INT, units="nano-bazers", value_description="Should be an integer between 2 and 2000") self.param_dict.add( "bat", r'.*bat=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, default_value=20, visibility=ParameterDictVisibility.READ_ONLY, get_timeout=10, set_timeout=20, display_name="Bat", description="The bat parameter", type=ParameterDictType.INT, units="nano-batbit", value_description="Should be an integer between 1 and 1000") self.param_dict.add("qux", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.READ_ONLY) self.param_dict.add("pho", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add("dil", r'.*qux=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), startup_param=False, visibility=ParameterDictVisibility.IMMUTABLE) self.param_dict.add( "qut", r'.*qut=(\d+).*', lambda match: int(match.group(1)), lambda x: str(x), direct_access=True, default_value=[10, 100], visibility=ParameterDictVisibility.DIRECT_ACCESS, expiration=1, get_timeout=10, set_timeout=20, display_name="Qut", description="The qut list parameter", type=ParameterDictType.LIST, units="nano-qutters", value_description= "Should be a 2-10 element list of integers between 2 and 2000") self.target_schema = { "bar": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 15 }, "visibility": "READ_WRITE", "range": None, }, "bat": { "description": "The bat parameter", "direct_access": False, "display_name": "Bat", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": 20, "description": "Should be an integer between 1 and 1000", "type": "int", "units": "nano-batbit" }, "visibility": "READ_ONLY", "range": None, }, "baz": { "description": "The baz parameter", "direct_access": True, "display_name": "Baz", "get_timeout": 30, "set_timeout": 40, "startup": False, "value": { "default": 20, "description": "Should be an integer between 2 and 2000", "type": "int", "units": "nano-bazers" }, "visibility": "DIRECT_ACCESS", "range": None, }, "dil": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "foo": { "direct_access": True, "get_timeout": 10, "set_timeout": 10, "startup": True, "value": { "default": 10 }, "visibility": "READ_WRITE", "range": None, }, "pho": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "IMMUTABLE", "range": None, }, "qut": { "description": "The qut list parameter", "direct_access": True, "display_name": "Qut", "get_timeout": 10, "set_timeout": 20, "startup": False, "value": { "default": [10, 100], "description": "Should be a 2-10 element list of integers between 2 and 2000", "type": "list", "units": "nano-qutters" }, "visibility": "DIRECT_ACCESS", "range": None, }, "qux": { "direct_access": False, "get_timeout": 10, "set_timeout": 10, "startup": False, "value": {}, "visibility": "READ_ONLY", "range": None, } } self.test_yaml = '''
class InstrumentProtocol(object): """ Base instrument protocol class. """ def __init__(self, driver_event): """ Base constructor. @param driver_event The callback for asynchronous driver events. """ # Event callback to send asynchronous events to the agent. self._driver_event = driver_event # The connection used to talk to the device. self._connection = None # The protocol state machine. self._protocol_fsm = None # The parameter dictionary. self._param_dict = ProtocolParameterDict() # The spot to stash a configuration before going into direct access # mode self._pre_direct_access_config = None # Driver configuration passed from the user self._startup_config = {} # scheduler config is a bit redundant now, but if we ever want to # re-initialize a scheduler we will need it. self._scheduler = None self._scheduler_callback = {} self._scheduler_config = {} ######################################################################## # Helper methods ######################################################################## def got_data(self, port_agent_packet): """ Called by the instrument connection when data is available. Defined in subclasses. """ log.error("base got_data. Who called me?") pass def _extract_sample(self, particle_class, regex, line, timestamp, publish=True): """ Extract sample from a response line if present and publish parsed particle @param particle_class The class to instantiate for this specific data particle. Parameterizing this allows for simple, standard behavior from this routine @param regex The regular expression that matches a data sample @param line string to match for sample. @param timestamp port agent timestamp to include with the particle @param publish boolean to publish samples (default True). If True, two different events are published: one to notify raw data and the other to notify parsed data. @retval dict of dicts {'parsed': parsed_sample, 'raw': raw_sample} if the line can be parsed for a sample. Otherwise, None. @todo Figure out how the agent wants the results for a single poll and return them that way from here """ sample = None if regex.match(line): particle = particle_class(line, port_timestamp=timestamp) parsed_sample = particle.generate() if publish and self._driver_event: self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample) sample = json.loads(parsed_sample) return sample return sample def get_current_state(self): """ Return current state of the protocol FSM. """ return self._protocol_fsm.get_current_state() def get_resource_capabilities(self, current_state=True): """ """ res_cmds = self._protocol_fsm.get_events(current_state) res_cmds = self._filter_capabilities(res_cmds) res_params = self._param_dict.get_keys() return [res_cmds, res_params] def _filter_capabilities(self, events): """ """ return events ######################################################################## # Scheduler interface. ######################################################################## def _add_scheduler(self, name, callback): """ Stage a scheduler in a driver. The job will actually be configured and started by initialize_scheduler @param name the name of the job @param callback the handler when the job is triggered @raise KeyError if we try to add a job twice """ if(self._scheduler_callback.get(name)): raise KeyError("duplicate scheduler exists for '%s'" % name) log.debug("Add scheduler callback: %s" % name) self._scheduler_callback[name] = callback self._add_scheduler_job(name) def _add_scheduler_event(self, name, event): """ Create a scheduler, but instead of passing a callback we pass in an event to raise. A callback function is dynamically created to do this. @param name the name of the job @param event: event to raise when the scheduler is triggered @raise KeyError if we try to add a job twice """ # Create a callback for the scheduler to raise an event def event_callback(self, event): log.info("driver job triggered, raise event: %s" % event) self._protocol_fsm.on_event(event) # Dynamically create the method and add it method = partial(event_callback, self, event) self._add_scheduler(name, method) def _add_scheduler_job(self, name): """ Map the driver configuration to a scheduler configuration. If the scheduler has been started then also add the job. @param name the name of the job @raise KeyError if job name does not exists in the callback config @raise KeyError if job is already configured """ # Do nothing if the scheduler isn't initialized if(not self._scheduler): return callback = self._scheduler_callback.get(name) if(not callback): raise KeyError("callback not defined in driver for '%s'" % name) if(self._scheduler_config.get(name)): raise KeyError("scheduler job already configured '%s'" % name) scheduler_config = self._get_scheduler_config() log.debug("Scheduler config: %s" % scheduler_config) # No config? Nothing to do then. if(scheduler_config == None): return job_config = scheduler_config.get(name) if(job_config): # Store the scheduler configuration self._scheduler_config[name] = { DriverSchedulerConfigKey.TRIGGER: job_config.get(DriverSchedulerConfigKey.TRIGGER), DriverSchedulerConfigKey.CALLBACK: callback } config = {name: self._scheduler_config[name]} log.debug("Scheduler job with config: %s" % config) # start the job. Note, this lazily starts the scheduler too :) self._scheduler.add_config(config) def _get_scheduler_config(self): """ Get the configuration dictionary to use for initializing jobs Returned dictionary structure: { 'job_name': { DriverSchedulerConfigKey.TRIGGER: {} } } @return: scheduler configuration dictionary """ # Currently the startup config is in the child class. # @TODO should the config code be promoted? config = self._startup_config return config.get(DriverConfigKey.SCHEDULER) def initialize_scheduler(self): """ Activate all configured schedulers added using _add_scheduler. Timers start when the job is activated. """ log.debug("Scheduler config: %s" % self._get_scheduler_config()) log.debug("Scheduler callbacks: %s" % self._scheduler_callback) self._scheduler = DriverScheduler() for name in self._scheduler_callback.keys(): log.debug("Add job for callback: %s" % name) self._add_scheduler_job(name) ############################################################# # Configuration logic ############################################################# def apply_startup_params(self): """ Apply the startup values previously stored in the protocol to the running config of the live instrument. The startup values are the values that are (1) marked as startup parameters and are (2) the "best" value to use at startup. Preference is given to the previously-set init value, then the default value, then the currently used value. This default method assumes a dict of parameter name and value for the configuration. This is the base stub for applying startup parameters at the protocol layer. @raise InstrumentParameterException If the config cannot be applied @raise NotImplementedException In the base class it isnt implemented """ raise NotImplementedException("Base class does not implement apply_startup_params()") def set_init_params(self, config): """ Set the initialization parameters to the given values in the protocol parameter dictionary. @param config The parameter_name/value to set in the initialization fields of the parameter dictionary @raise InstrumentParameterException If the config cannot be set """ if not isinstance(config, dict): raise InstrumentParameterException("Invalid init config format") self._startup_config = config param_config = config.get(DriverConfigKey.PARAMETERS) if(param_config): for name in param_config.keys(): log.debug("Setting init value for %s to %s", name, param_config[name]) self._param_dict.set_init_value(name, param_config[name]) def get_startup_config(self): """ Gets the startup configuration for the instrument. The parameters returned are marked as startup, and the values are the best as chosen from the initialization, default, and current parameters. @retval The dict of parameter_name/values (override this method if it is more involved for a specific instrument) that should be set at a higher level. @raise InstrumentProtocolException if a startup parameter doesn't have a init or default value """ return_dict = {} start_list = self._param_dict.get_keys() log.trace("Startup list: %s", start_list) assert isinstance(start_list, list) for param in start_list: result = self._param_dict.get_config_value(param) if(result != None): return_dict[param] = result elif(self._param_dict.is_startup_param(param)): raise InstrumentProtocolException("Required startup value not specified: %s" % param) return return_dict def get_direct_access_params(self): """ Get the list of direct access parameters, useful for restoring direct access configurations up in the driver. @retval a list of direct access parameter names """ return self._param_dict.get_direct_access_list() def get_cached_config(self): """ Return the configuration object that shows the instrument's configuration as cached in the parameter dictionary...usually in sync with the instrument, but accessible when offline... @retval The cached configuration in the instruments config format. By default, it is a dictionary of parameter names and values. """ assert self._param_dict != None return self._param_dict.get_config() ######################################################################## # Command build and response parse handlers. ######################################################################## def _add_response_handler(self, cmd, func, state=None): """ Insert a handler class responsible for handling the response to a command sent to the instrument, optionally available only in a specific state. @param cmd The high level key of the command to respond to. @param func The function that handles the response @param state The state to pair with the command for which the function should be used """ if state == None: self._response_handlers[cmd] = func else: self._response_handlers[(state, cmd)] = func def _add_build_handler(self, cmd, func): """ Add a command building function. @param cmd The device command to build. @param func The function that constructs the command. """ self._build_handlers[cmd] = func ######################################################################## # Helpers to build commands. ######################################################################## def _build_simple_command(self, cmd, *args): """ Builder for simple commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s%s" % (cmd, self._newline) def _build_keypress_command(self, cmd, *args): """ Builder for simple, non-EOLN-terminated commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s" % (cmd) def _build_multi_keypress_command(self, cmd, *args): """ Builder for simple, non-EOLN-terminated commands @param cmd The command to build @param args Unused arguments @retval Returns string ready for sending to instrument """ return "%s%s%s%s%s%s" % (cmd, cmd, cmd, cmd, cmd, cmd) ######################################################################## # Static helpers to format set commands. ######################################################################## def _true_false_to_string(v): """ Write a boolean value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v a boolean value. @retval A yes/no string formatted as a Python boolean for set operations. @throws InstrumentParameterException if value not a bool. """ if not isinstance(v,bool): raise InstrumentParameterException('Value %s is not a bool.' % str(v)) return str(v) @staticmethod def _int_to_string(v): """ Write an int value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v An int val. @retval an int string formatted for generic set operations. @throws InstrumentParameterException if value not an int. """ if not isinstance(v,int): raise InstrumentParameterException('Value %s is not an int.' % str(v)) else: return '%i' % v @staticmethod def _float_to_string(v): """ Write a float value to string formatted for "generic" set operations. Subclasses should overload this as needed for instrument-specific formatting. @param v A float val. @retval a float string formatted for "generic" set operations. @throws InstrumentParameterException if value is not a float. """ if not isinstance(v,float): raise InstrumentParameterException('Value %s is not a float.' % v) else: return '%e' % v
class DataSetDriver(object): """ Base class for data set drivers. Provides: - an interface via callback to publish data - an interface via callback to persist driver state - an interface via callback to handle exceptions - an start and stop sampling - a client interface for execute resource Subclasses need to include harvesters and parsers and be specialized to handle the interaction between the two. Configurations should contain keys from the DataSetDriverConfigKey class and should look something like this example (more full documentation in the "Dataset Agent Architecture" page on the OOI wiki): { 'harvester': { 'directory': '/tmp/dsatest', 'pattern': '*.txt', 'frequency': 1, }, 'parser': {} 'driver': { 'records_per_second' 'harvester_polling_interval' 'batched_particle_count' } } """ def __init__(self, config, memento, data_callback, state_callback, exception_callback): self._config = config self._data_callback = data_callback self._state_callback = state_callback self._exception_callback = exception_callback self._memento = memento self._publisher_thread = None self._verify_config() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._param_dict = ProtocolParameterDict() self._cmd_dict = ProtocolCommandDict() self._driver_dict = DriverDict() self._build_command_dict() self._build_driver_dict() self._build_param_dict() def shutdown(self): self.stop_sampling() def start_sampling(self): """ Start a new thread to monitor for data """ self._start_sampling() self._start_publisher_thread() def stop_sampling(self): """ Stop the sampling thread """ log.debug("Stopping driver now") self._stop_sampling() self._stop_publisher_thread() def _start_sampling(self): raise NotImplementedException('virtual method needs to be specialized') def _stop_sampling(self): raise NotImplementedException('virtual method needs to be specialized') def _is_sampling(self): """ Currently the drivers only have two states, command and streaming and all resource commands are common, either start or stop autosample. Therefore we didn't implement an enitre state machine to manage states and commands. If it does get more complex than this we should take the time to implement a state machine to add some flexibility """ raise NotImplementedException('virtual method needs to be specialized') def cmd_dvr(self, cmd, *args, **kwargs): log.warn("DRIVER: cmd_dvr %s", cmd) if cmd == 'execute_resource': resource_cmd = args[0] if resource_cmd == DriverEvent.START_AUTOSAMPLE: return (ResourceAgentState.STREAMING, None) elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE: self.stop_sampling() return (ResourceAgentState.COMMAND, None) else: log.error("Unhandled resource command: %s", resource_cmd) raise elif cmd == 'get_resource_capabilities': return self.get_resource_capabilities() elif cmd == 'set_resource': return self.set_resource(*args, **kwargs) elif cmd == 'get_resource': return self.get_resource(*args, **kwargs) elif cmd == 'get_config_metadata': return self.get_config_metadata(*args, **kwargs) elif cmd == 'disconnect': pass elif cmd == 'initialize': pass else: log.error("Unhandled command: %s", cmd) raise InstrumentStateException("Unhandled command: %s" % cmd) def get_resource_capabilities(self, current_state=True, *args, **kwargs): """ Return driver commands and parameters. @param current_state True to retrieve commands available in current state, otherwise reutrn all commands. @retval list of AgentCapability objects representing the drivers capabilities. @raises NotImplementedException if not implemented by subclass. """ res_params = self._param_dict.get_keys() res_cmds = [DriverEvent.STOP_AUTOSAMPLE, DriverEvent.START_AUTOSAMPLE] if current_state and self._is_sampling(): res_cmds = [DriverEvent.STOP_AUTOSAMPLE] elif current_state and not self._is_sampling(): res_cmds = [DriverEvent.START_AUTOSAMPLE] return [res_cmds, res_params] def set_resource(self, *args, **kwargs): """ Set the driver parameter """ log.trace("start set_resource") try: params = args[0] except IndexError: raise InstrumentParameterException('Set command requires a parameter dict.') log.trace("set_resource: iterate through params: %s", params) for (key, val) in params.iteritems(): if key in [DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND]: if not isinstance(val, int): raise InstrumentParameterException("%s must be an integer" % key) if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]: if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key) if val <= 0: raise InstrumentParameterException("%s must be > 0" % key) self._param_dict.set_value(key, val) # Set the driver parameters self._generate_particle_count = self._param_dict.get(DriverParameter.BATCHED_PARTICLE_COUNT) self._particle_count_per_second = self._param_dict.get(DriverParameter.RECORDS_PER_SECOND) self._polling_interval = self._param_dict.get(DriverParameter.PUBLISHER_POLLING_INTERVAL) log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second, self._generate_particle_count) def get_resource(self, *args, **kwargs): """ Get driver parameter """ result = {} try: params = args[0] except IndexError: raise InstrumentParameterException('Set command requires a parameter list.') # If all params requested, retrieve config. if params == DriverParameter.ALL: result = self._param_dict.get_config() # If not all params, confirm a list or tuple of params to retrieve. # Raise if not a list or tuple. # Retrieve each key in the list, raise if any are invalid. else: if not isinstance(params, (list, tuple)): raise InstrumentParameterException('Get argument not a list or tuple.') result = {} for key in params: try: val = self._param_dict.get(key) result[key] = val except KeyError: raise InstrumentParameterException(('%s is not a valid parameter.' % key)) return result def get_config_metadata(self): """ Return the configuration metadata object in JSON format @retval The description of the parameters, commands, and driver info in a JSON string @see https://confluence.oceanobservatories.org/display/syseng/CIAD+MI+SV+Instrument+Driver-Agent+parameter+and+command+metadata+exchange """ log.debug("Getting metadata from driver...") log.debug("Getting metadata dict from protocol...") return_dict = {} return_dict[ConfigMetadataKey.DRIVER] = self._driver_dict.generate_dict() return_dict[ConfigMetadataKey.COMMANDS] = self._cmd_dict.generate_dict() return_dict[ConfigMetadataKey.PARAMETERS] = self._param_dict.generate_dict() return return_dict def _verify_config(self): """ virtual method to verify the supplied driver configuration is value. Must be overloaded in sub classes. raises an ConfigurationException when a configuration error is detected. """ raise NotImplementedException('virtual methond needs to be specialized') def _build_driver_dict(self): """ Populate the driver dictionary with options """ pass def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(DriverEvent.START_AUTOSAMPLE, display_name="start autosample") self._cmd_dict.add(DriverEvent.STOP_AUTOSAMPLE, display_name="stop autosample") def _build_param_dict(self): """ Setup three common driver parameters """ self._param_dict.add_parameter( Parameter( DriverParameter.RECORDS_PER_SECOND, int, value=60, type=ParameterDictType.INT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Records Per Second", description="Number of records to process per second") ) self._param_dict.add_parameter( Parameter( DriverParameter.PUBLISHER_POLLING_INTERVAL, float, value=1, type=ParameterDictType.FLOAT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Harvester Polling Interval", description="Duration in minutes to wait before checking for new files.") ) self._param_dict.add_parameter( Parameter( DriverParameter.BATCHED_PARTICLE_COUNT, int, value=1, type=ParameterDictType.INT, visibility=ParameterDictVisibility.IMMUTABLE, display_name="Batched Particle Count", description="Number of particles to batch before sending to the agent") ) config = self._config.get(DataSourceConfigKey.DRIVER, {}) log.debug("set_resource on startup with: %s", config) self.set_resource(config) def _start_publisher_thread(self): self._publisher_thread = gevent.spawn(self._publisher_loop) self._publisher_shutdown = False def _stop_publisher_thread(self): log.debug("Signal shutdown") self._publisher_shutdown = True if self._publisher_thread: self._publisher_thread.kill(block=False) log.debug("shutdown complete") def _publisher_loop(self): """ Main loop to listen for new files to parse. Parse them and move on. """ log.info("Starting main publishing loop") try: while(not self._publisher_shutdown): self._poll() gevent.sleep(self._polling_interval) except Exception as e: log.error("Exception in publisher thread: %s", e) self._exception_callback(e) log.debug("publisher thread detected shutdown request") def _poll(self): raise NotImplementedException('virtual methond needs to be specialized') def _new_file_exception(self): raise NotImplementedException('virtual methond needs to be specialized')
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', # this is a driver only parameter None, int, type=ParameterDictType.INT, startup_param=True, display_name='D1000 Sample Periodicity', range=(1, 3600), description='Periodicity of D1000 temperature sample in autosample mode: (1-3600)', default_value=DEFAULT_SAMPLE_RATE, units=Units.SECOND, visibility=ParameterDictVisibility.READ_WRITE) self._add_setup_param(Parameter.CHANNEL_ADDRESS, int, type=ParameterDictType.INT, display_name='Base Channel Address', description='Hex value of ASCII character to ID unit, e.g. 31 is the ASCII code for 1:' ' (30-31, 41-5A, 61-7A)', range=(0x30, 0x7A), default_value=0x31) self._add_setup_param(Parameter.LINEFEED, bool, type=ParameterDictType.BOOL, display_name='Line Feed Flag', range={'True': True, 'False': False}, description='Enable D1000 to generate a linefeed before and after each response:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.PARITY_TYPE, bool, type=ParameterDictType.BOOL, display_name='Parity Type', range={'Odd': True, 'Even': False}, description='Sets the parity: (true:odd | false:even)', default_value=False) self._add_setup_param(Parameter.PARITY_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Parity Flag', range={'True': True, 'False': False}, description='Enable use of parity bit, a parity error will be issued if detected:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.EXTENDED_ADDRESSING, bool, type=ParameterDictType.BOOL, display_name='Extended Addressing', range={'True': True, 'False': False}, description='Enable extended addressing: (true | false)', default_value=False) self._add_setup_param(Parameter.BAUD_RATE, int, type=ParameterDictType.INT, display_name='Baud Rate', range={'38400': 0, '19200': 1, '9600': 2, '4800': 3, '2400': 4, '1200': 5, '600': 6, '300': 7, '57600': 8}, description='Using ethernet interface in deployed configuration: (300, 600, ' '1200, 2400, 4800, 9600, 19200, 38400, 57600)', default_value=9600, units=Units.BAUD) self._add_setup_param(Parameter.ALARM_ENABLE, bool, type=ParameterDictType.BOOL, display_name='Enable Alarms', range={'True': True, 'False': False}, description='Enable alarms to be controlled by the Digital Output (DO) command:' ' (true | false)', default_value=False) self._add_setup_param(Parameter.LOW_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='Low Alarm Latching', range={'True': True, 'False': False}, description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.HIGH_ALARM_LATCH, bool, type=ParameterDictType.BOOL, display_name='High Alarm Latching', range={'True': True, 'False': False}, description='Enable changing the alarm to latching mode: (true | false)', default_value=False) self._add_setup_param(Parameter.RTD_4_WIRE, bool, type=ParameterDictType.BOOL, display_name='4 Wire RTD Flag', range={'True': True, 'False': False}, description='Represents a physical configuration of the instrument, ' 'disabling may cause data to be misaligned: (true | false)', default_value=True) self._add_setup_param(Parameter.TEMP_UNITS, bool, type=ParameterDictType.BOOL, display_name='Fahrenheit Flag', range={'Fahrenheit': True, 'Celsius': False}, description='Flag to control the temperature format: (true:Fahrenheit | false:Celsius)', default_value=False) self._add_setup_param(Parameter.ECHO, bool, type=ParameterDictType.BOOL, display_name='Daisy Chain', range={'True': True, 'False': False}, description='If not set, only 1 out of 3 D1000s will process commands: (true | false)', default_value=True) self._add_setup_param(Parameter.COMMUNICATION_DELAY, int, type=ParameterDictType.INT, display_name='Communication Delay', range=(0, 3), description='The number of delays to add when processing commands: (0-3)', default_value=0) self._add_setup_param(Parameter.PRECISION, int, type=ParameterDictType.INT, display_name='Precision', range={'4 digits': 0, '5 digits': 1, '6 digits': 2, '7 digits': 3}, description='Number of digits the instrument should output for temperature query: ' '(0=4-3=7)', default_value=6) self._add_setup_param(Parameter.LARGE_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Large Signal Filter Constant', range={'0': 0, '.25': 1, '.5': 2, '1': 3, '2': 4, '4': 5, '8': 6, '16': 7}, description='Time to reach 63% of its final value: ' '(0 = 0.0, 1 = 0.25, 2 = 0.5, 3 = 1.0, 4 = 2.0, 5 = 4.0, 6 = 8.0, 7 = 16.0)', default_value=0.0, units=Units.SECOND) self._add_setup_param(Parameter.SMALL_SIGNAL_FILTER_C, float, type=ParameterDictType.FLOAT, display_name='Small Signal Filter Constant', range={'0': 0, '.25': 1, '.5': 2, '1': 3, '2': 4, '4': 5, '8': 6, '16': 7}, description='Smaller filter constant, should be larger than large filter constant: ' '(0 = 0.0, 1 = 0.25, 2 = 0.5, 3 = 1.0, 4 = 2.0, 5 = 4.0, 6 = 8.0, 7 = 16.0)', default_value=0.50, units=Units.SECOND) for key in self._param_dict.get_keys(): self._param_dict.set_default(key)
class Protocol(WorkhorseProtocol): def __init__(self, prompts, newline, driver_event, connections=None): """ Constructor. @param prompts Enum class containing possible device prompts used for command response logic. @param newline The device newline. @driver_event The callback for asynchronous driver events. """ if not type(connections) is list: raise InstrumentProtocolException( 'Unable to instantiate multi connection protocol without connection list' ) self._param_dict2 = ProtocolParameterDict() # Construct superclass. WorkhorseProtocol.__init__(self, prompts, newline, driver_event) # Create multiple connection versions of the pieces of protocol involving data to/from the instrument self._linebuf = {connection: '' for connection in connections} self._promptbuf = {connection: '' for connection in connections} self._last_data_timestamp = { connection: None for connection in connections } self.connections = {connection: None for connection in connections} self.chunkers = { connection: StringChunker(self.sieve_function) for connection in connections } def _get_response(self, timeout=10, expected_prompt=None, response_regex=None, connection=None): """ Overridden to handle multiple port agent connections """ if connection is None: raise InstrumentProtocolException( '_get_response: no connection supplied!') # Grab time for timeout and wait for prompt. end_time = time.time() + timeout if response_regex and not isinstance(response_regex, RE_PATTERN): raise InstrumentProtocolException( 'Response regex is not a compiled pattern!') if expected_prompt and response_regex: raise InstrumentProtocolException( 'Cannot supply both regex and expected prompt!') if expected_prompt is None: prompt_list = self._get_prompts() else: if isinstance(expected_prompt, str): prompt_list = [expected_prompt] else: prompt_list = expected_prompt if response_regex is None: pattern = None else: pattern = response_regex.pattern log.debug( '_get_response: timeout=%s, prompt_list=%s, expected_prompt=%r, response_regex=%r, promptbuf=%r', timeout, prompt_list, expected_prompt, pattern, self._promptbuf) while time.time() < end_time: if response_regex: # noinspection PyArgumentList match = response_regex.search(self._linebuf[connection]) if match: return match.groups() else: for item in prompt_list: index = self._promptbuf[connection].find(item) if index >= 0: result = self._promptbuf[connection][0:index + len(item)] return item, result time.sleep(.1) raise InstrumentTimeoutException( "in InstrumentProtocol._get_response()") def _do_cmd_resp(self, cmd, *args, **kwargs): """ Overridden to handle multiple port agent connections """ connection = kwargs.get('connection') if connection is None: raise InstrumentProtocolException( '_do_cmd_resp: no connection supplied!') # Get timeout and initialize response. timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT) expected_prompt = kwargs.get('expected_prompt', None) response_regex = kwargs.get('response_regex', None) if response_regex and not isinstance(response_regex, RE_PATTERN): raise InstrumentProtocolException( 'Response regex is not a compiled pattern!') if expected_prompt and response_regex: raise InstrumentProtocolException( 'Cannot supply both regex and expected prompt!') self._do_cmd_no_resp(cmd, *args, **kwargs) # Wait for the prompt, prepare result and return, timeout exception if response_regex: prompt = "" result_tuple = self._get_response(timeout, connection=connection, response_regex=response_regex, expected_prompt=expected_prompt) result = "".join(result_tuple) else: (prompt, result) = self._get_response(timeout, connection=connection, expected_prompt=expected_prompt) resp_handler = self._response_handlers.get( (self.get_current_state(), cmd), self._response_handlers.get(cmd, None)) resp_result = None if callable(resp_handler): resp_result = resp_handler(result, prompt) return resp_result def _send_data(self, data, write_delay=0, connection=None): if connection is None: raise InstrumentProtocolException( '_send_data: no connection supplied!') if write_delay == 0: self.connections[connection].send(data) else: for char in data: self.connections[connection].send(char) time.sleep(write_delay) def _do_cmd_no_resp(self, cmd, *args, **kwargs): """ Overridden to handle multiple port agent connections """ connection = kwargs.get('connection') if connection is None: raise InstrumentProtocolException( '_do_cmd_no_resp: no connection supplied! %r %r %r' % (cmd, args, kwargs)) timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT) write_delay = kwargs.get('write_delay', DEFAULT_WRITE_DELAY) build_handler = self._build_handlers.get(cmd, None) if not callable(build_handler): log.error('_do_cmd_no_resp: no handler for command: %s' % cmd) raise InstrumentProtocolException( error_code=InstErrorCode.BAD_DRIVER_COMMAND) cmd_line = build_handler(cmd, *args) # Wakeup the device, timeout exception as needed self._wakeup(timeout, connection=connection) # Clear line and prompt buffers for result, then send command. self._linebuf[connection] = '' self._promptbuf[connection] = '' self._send_data(cmd_line, write_delay, connection=connection) def _do_cmd_direct(self, cmd, connection=None): """ Issue an untranslated command to the instrument. No response is handled as a result of the command. @param cmd The high level command to issue """ # Send command. self._send_data(cmd, connection=connection) ######################################################################## # Incoming data (for parsing) callback. ######################################################################## def got_data(self, port_agent_packet, connection=None): """ Called by the instrument connection when data is available. Append line and prompt buffers. Also add data to the chunker and when received call got_chunk to publish results. :param connection: connection which produced this packet :param port_agent_packet: packet of data """ if connection is None: raise InstrumentProtocolException( 'got_data: no connection supplied!') data_length = port_agent_packet.get_data_length() data = port_agent_packet.get_data() timestamp = port_agent_packet.get_timestamp() log.debug("Got Data: %r %r", connection, data) log.debug("Add Port Agent Timestamp: %r %s", connection, timestamp) if data_length > 0: if self.get_current_state() == DriverProtocolState.DIRECT_ACCESS: self._driver_event(DriverAsyncEvent.DIRECT_ACCESS, data) self.add_to_buffer(data, connection=connection) self.chunkers[connection].add_chunk(data, timestamp) (timestamp, chunk) = self.chunkers[connection].get_next_data() while chunk: self._got_chunk(chunk, timestamp, connection=connection) (timestamp, chunk) = self.chunkers[connection].get_next_data() ######################################################################## # Incoming raw data callback. ######################################################################## def got_raw(self, port_agent_packet, connection=None): """ Called by the port agent client when raw data is available, such as data sent by the driver to the instrument, the instrument responses,etc. :param connection: connection which produced this packet :param port_agent_packet: packet of data """ self.publish_raw(port_agent_packet, connection) def publish_raw(self, port_agent_packet, connection=None): pass def add_to_buffer(self, data, connection=None): """ Add a chunk of data to the internal data buffers buffers implemented as lifo ring buffer :param data: bytes to add to the buffer :param connection: connection which produced this packet """ # Update the line and prompt buffers. self._linebuf[connection] += data self._promptbuf[connection] += data self._last_data_timestamp[connection] = time.time() # If our buffer exceeds the max allowable size then drop the leading # characters on the floor. if len(self._linebuf[connection]) > self._max_buffer_size(): self._linebuf[connection] = self._linebuf[connection][ self._max_buffer_size() * -1:] # If our buffer exceeds the max allowable size then drop the leading # characters on the floor. if len(self._promptbuf[connection]) > self._max_buffer_size(): self._promptbuf[connection] = self._promptbuf[connection][ self._max_buffer_size() * -1:] log.debug("LINE BUF: %r", self._linebuf[connection][-50:]) log.debug("PROMPT BUF: %r", self._promptbuf[connection][-50:]) ######################################################################## # Wakeup helpers. ######################################################################## def _send_wakeup(self, connection=None): """ Send a wakeup to the device. Overridden by device specific subclasses. """ self.connections[connection].send(NEWLINE) def _wakeup(self, timeout, delay=1, connection=None): """ Clear buffers and send a wakeup command to the instrument @param timeout The timeout to wake the device. @param delay The time to wait between consecutive wakeups. @throw InstrumentTimeoutException if the device could not be woken. """ if connection is None: raise InstrumentProtocolException( '_wakeup: no connection supplied!') # Clear the prompt buffer. log.trace("clearing promptbuf: %r", self._promptbuf) self._promptbuf[connection] = '' # Grab time for timeout. starttime = time.time() while True: # Send a line return and wait a sec. log.trace('Sending wakeup. timeout=%s', timeout) self._send_wakeup(connection=connection) time.sleep(delay) log.trace("Prompts: %s", self._get_prompts()) for item in self._get_prompts(): log.trace("buffer: %r", self._promptbuf[connection]) log.trace("find prompt: %r", item) index = self._promptbuf[connection].find(item) log.trace("Got prompt (index: %s): %r ", index, self._promptbuf[connection]) if index >= 0: log.trace('wakeup got prompt: %r', item) return item log.trace("Searched for all prompts") if time.time() > starttime + timeout: raise InstrumentTimeoutException("in _wakeup()") def _build_param_dict(self): # We're going to build two complete sets of ADCP parameters here # one set for the master instrument and one for the slave for param in parameter_regexes: self._param_dict.add( param, parameter_regexes.get(param), parameter_extractors.get(param), parameter_formatters.get(param), type=parameter_types.get(param), display_name=parameter_names.get(param), value_description=parameter_descriptions.get(param), startup_param=parameter_startup.get(param, False), direct_access=parameter_direct.get(param, False), visibility=parameter_visibility.get( param, ParameterDictVisibility.READ_WRITE), default_value=master_parameter_defaults.get(param), units=parameter_units.get(param)) for param in parameter_regexes: # Scheduled events are handled by the master if WorkhorseEngineeringParameter.has(param): continue self._param_dict.add( param + '_5th', r'DONTMATCHMEIMNOTREAL!', parameter_extractors.get(param), parameter_formatters.get(param), type=parameter_types.get(param), display_name=parameter_names.get(param), value_description=parameter_descriptions.get(param), startup_param=parameter_startup.get(param, False), direct_access=parameter_direct.get(param, False), visibility=parameter_visibility.get( param, ParameterDictVisibility.READ_WRITE), default_value=slave_parameter_defaults.get(param), units=parameter_units.get(param)) self._param_dict.set_default(WorkhorseParameter.CLOCK_SYNCH_INTERVAL) self._param_dict.set_default(WorkhorseParameter.GET_STATUS_INTERVAL) # now we're going to build a whole 'nother param dict for the slave parameters # that contain regex values so we can fill them in easily... for param in parameter_regexes: # Scheduled events are handled by the master if WorkhorseEngineeringParameter.has(param): continue self._param_dict2.add(param + '_5th', parameter_regexes.get(param), parameter_extractors.get(param), parameter_formatters.get(param)) # ####################################################################### # Private helpers. # ####################################################################### def _got_chunk(self, chunk, timestamp, connection=None): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ if ADCP_PD0_PARSED_REGEX_MATCHER.match(chunk): pd0 = AdcpPd0Record(chunk) transform = pd0.coord_transform.coord_transform # Only BEAM transform supported for VADCP if transform != particles.Pd0CoordinateTransformType.BEAM: raise SampleException( 'Received unsupported coordinate transform type: %s' % transform) if connection == SlaveProtocol.FOURBEAM: science = particles.VadcpBeamMasterParticle( pd0, port_timestamp=timestamp).generate() config = particles.AdcpPd0ConfigParticle( pd0, port_timestamp=timestamp).generate() engineering = particles.AdcpPd0EngineeringParticle( pd0, port_timestamp=timestamp).generate() else: science = particles.VadcpBeamSlaveParticle( pd0, port_timestamp=timestamp).generate() config = particles.VadcpConfigSlaveParticle( pd0, port_timestamp=timestamp).generate() engineering = particles.VadcpEngineeringSlaveParticle( pd0, port_timestamp=timestamp).generate() out_particles = [science] for particle in [config, engineering]: if self._changed(particle): out_particles.append(particle) for particle in out_particles: self._driver_event(DriverAsyncEvent.SAMPLE, particle) else: if connection == SlaveProtocol.FIFTHBEAM: if self._extract_sample( particles.VadcpCompassCalibrationDataParticle, ADCP_COMPASS_CALIBRATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample( particles.VadcpSystemConfigurationDataParticle, ADCP_SYSTEM_CONFIGURATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample( particles.VadcpAncillarySystemDataParticle, ADCP_ANCILLARY_SYSTEM_DATA_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(particles.VadcpTransmitPathParticle, ADCP_TRANSMIT_PATH_REGEX_MATCHER, chunk, timestamp): return elif connection == SlaveProtocol.FOURBEAM: if self._extract_sample( particles.AdcpCompassCalibrationDataParticle, ADCP_COMPASS_CALIBRATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample( particles.AdcpSystemConfigurationDataParticle, ADCP_SYSTEM_CONFIGURATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample( particles.AdcpAncillarySystemDataParticle, ADCP_ANCILLARY_SYSTEM_DATA_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(particles.AdcpTransmitPathParticle, ADCP_TRANSMIT_PATH_REGEX_MATCHER, chunk, timestamp): return def _send_break_cmd(self, delay, connection=None): """ Send a BREAK to attempt to wake the device. """ self.connections[connection].send_break(delay) def _sync_clock(self, command, date_time_param, timeout=TIMEOUT, delay=1, time_format="%Y/%m/%d,%H:%M:%S"): """ Send the command to the instrument to synchronize the clock @param command set command @param date_time_param: date time parameter that we want to set @param timeout: command timeout @param delay: wakeup delay @param time_format: time format string for set command """ log.info("SYNCING TIME WITH SENSOR.") for connection in self.connections: self._do_cmd_resp(command, date_time_param, get_timestamp_delayed("%Y/%m/%d, %H:%M:%S"), timeout=timeout, connection=connection) # ####################################################################### # Startup parameter handlers ######################################################################## def _get_params(self, parameters, connection): command = NEWLINE.join(['%s?' % p for p in parameters]) + NEWLINE if len(parameters) > 1: regex = re.compile( r'(%s.*?%s.*?>)' % (parameters[0], parameters[-1]), re.DOTALL) else: regex = re.compile(r'(%s.*?>)' % parameters[0], re.DOTALL) self._linebuf[connection] = '' self._promptbuf[connection] = '' self._do_cmd_direct(command, connection=connection) return self._get_response(response_regex=regex, connection=connection) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ # see if we passed in a list of parameters to query # if not, use the whole parameter list parameters = kwargs.get('params') if parameters is None or WorkhorseParameter.ALL in parameters: parameters = self._param_dict.get_keys() # filter out the engineering parameters and ALL parameters = [ p for p in parameters if not WorkhorseEngineeringParameter.has(p) and p != WorkhorseParameter.ALL ] # Get old param dict config. old_config = self._param_dict.get_config() if parameters: # MASTER master_params = [p for p in parameters if '_5th' not in p] if master_params: resp = self._get_params(master_params, SlaveProtocol.FOURBEAM) self._param_dict.update_many(resp) # SLAVE slave_params = [ p.replace('_5th', '') for p in parameters if '_5th' in p ] if slave_params: resp = self._get_params(slave_params, SlaveProtocol.FIFTHBEAM) self._param_dict2.update_many(resp) for key, value in self._param_dict2.get_all().iteritems(): self._param_dict.set_value(key, value) new_config = self._param_dict.get_config() # Check if there is any changes. Ignore TT if not dict_equal(new_config, old_config, ['TT']) or kwargs.get('force'): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def _execute_set_params(self, commands, connection): if commands: # we are going to send the concatenation of all our set commands self._linebuf[connection] = '' self._do_cmd_direct(''.join(commands), connection=connection) # we'll need to build a regular expression to retrieve all of the responses # including any possible errors if len(commands) == 1: regex = re.compile(r'(%s.*?)\r\n>' % commands[-1].strip(), re.DOTALL) else: regex = re.compile( r'(%s.*?%s.*?)\r\n>' % (commands[0].strip(), commands[-1].strip()), re.DOTALL) response = self._get_response(response_regex=regex, connection=connection) self._parse_set_response(response[0], None) def _set_params(self, *args, **kwargs): """ Issue commands to the instrument to set various parameters """ self._verify_not_readonly(*args, **kwargs) params = args[0] changed = [] old_config = self._param_dict.get_config() master_commands = [] slave_commands = [] for key, val in params.iteritems(): if WorkhorseEngineeringParameter.has(key): continue if val != old_config.get(key): changed.append(key) if '_5th' in key: slave_commands.append( self._build_set_command(WorkhorseInstrumentCmds.SET, key.replace('_5th', ''), val)) else: master_commands.append( self._build_set_command(WorkhorseInstrumentCmds.SET, key, val)) self._execute_set_params(master_commands, connection=SlaveProtocol.FOURBEAM) self._execute_set_params(slave_commands, connection=SlaveProtocol.FIFTHBEAM) # Handle engineering parameters force = False if WorkhorseParameter.CLOCK_SYNCH_INTERVAL in params: if (params[WorkhorseParameter.CLOCK_SYNCH_INTERVAL] != self._param_dict.get( WorkhorseParameter.CLOCK_SYNCH_INTERVAL)): self._param_dict.set_value( WorkhorseParameter.CLOCK_SYNCH_INTERVAL, params[WorkhorseParameter.CLOCK_SYNCH_INTERVAL]) self.start_scheduled_job( WorkhorseParameter.CLOCK_SYNCH_INTERVAL, WorkhorseScheduledJob.CLOCK_SYNC, WorkhorseProtocolEvent.SCHEDULED_CLOCK_SYNC) force = True if WorkhorseParameter.GET_STATUS_INTERVAL in params: if (params[WorkhorseParameter.GET_STATUS_INTERVAL] != self._param_dict.get( WorkhorseParameter.GET_STATUS_INTERVAL)): self._param_dict.set_value( WorkhorseParameter.GET_STATUS_INTERVAL, params[WorkhorseParameter.GET_STATUS_INTERVAL]) self.start_scheduled_job( WorkhorseParameter.GET_STATUS_INTERVAL, WorkhorseScheduledJob.GET_CONFIGURATION, WorkhorseProtocolEvent.SCHEDULED_GET_STATUS) force = True self._update_params(params=changed, force=force) return None def _send_break(self, duration=1000, connection=None): """ Send a BREAK to attempt to wake the device. """ self._linebuf[connection] = '' self._promptbuf[connection] = '' self._send_break_cmd(duration, connection=connection) self._get_response(expected_prompt=WorkhorsePrompt.BREAK, connection=connection) def _start_logging(self, timeout=TIMEOUT, connection=None): """ Command the instrument to start logging @param timeout: how long to wait for a prompt @throws: InstrumentProtocolException if failed to start logging """ try: start = WorkhorseInstrumentCmds.START_LOGGING # start the slave first, it collects on signal from master self._do_cmd_resp(start, timeout=timeout, connection=SlaveProtocol.FIFTHBEAM) self._do_cmd_resp(start, timeout=timeout, connection=SlaveProtocol.FOURBEAM) except InstrumentException: self._stop_logging() raise def _stop_logging(self): # stop the master first (slave only collects on signal from master) self._send_break(connection=SlaveProtocol.FOURBEAM) self._send_break(connection=SlaveProtocol.FIFTHBEAM) def _discover(self, connection=None): """ Discover current state; can be COMMAND or AUTOSAMPLE or UNKNOWN. @return (next_protocol_state, next_agent_state) @throws InstrumentTimeoutException if the device cannot be woken. @throws InstrumentStateException if the device response does not correspond to an expected state. """ states = set() command = (WorkhorseProtocolState.COMMAND, ResourceAgentState.COMMAND) auto = (WorkhorseProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING) for connection in self.connections: try: self._wakeup(3, connection=connection) states.add(command) except InstrumentException: states.add(auto) if len(states) == 1: # states match, return this state return states.pop() # states don't match self._stop_logging() return command def _run_test(self, *args, **kwargs): kwargs['timeout'] = 30 kwargs['expected_prompt'] = WorkhorsePrompt.COMMAND result = [] for connection in self.connections: result.append(connection) kwargs['connection'] = connection result.append( self._do_cmd_resp(WorkhorseInstrumentCmds.RUN_TEST_200, *args, **kwargs)) return NEWLINE.join(result) @contextmanager def _pause_logging(self): try: self._stop_logging() yield finally: self._start_logging() ######################################################################## # COMMAND handlers. ######################################################################## def _handler_command_acquire_status(self, *args, **kwargs): """ execute a get status @return next_state, (next_agent_state, result) if successful. @throws InstrumentProtocolException from _do_cmd_resp. """ a = super(Protocol, self)._handler_command_acquire_status( connection=SlaveProtocol.FOURBEAM) b = super(Protocol, self)._handler_command_acquire_status( connection=SlaveProtocol.FIFTHBEAM) return None, (None, a[1][1] + b[1][1]) def _handler_command_recover_autosample(self): log.info( 'PD0 sample detected in COMMAND, not allowed in VADCP. Sending break' ) self._stop_logging() ###################################################### # DIRECT_ACCESS handlers ###################################################### def _handler_direct_access_execute_direct(self, data): next_state = None result = None next_agent_state = None self._do_cmd_direct(data) # add sent command to list for 'echo' filtering in callback self._sent_cmds.append(data) return next_state, (next_agent_state, result)
class Protocol(McLaneProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. McLaneProtocol.__init__(self, prompts, newline, driver_event) # TODO - reset next_port on mechanical refresh of the PPS filters - how is the driver notified? # TODO - need to persist state for next_port to save driver restart self.next_port = 1 # next available port # get the following: # - VERSION # - CAPACITY (pump flow) # - BATTERY # - CODES (termination codes) # - COPYRIGHT (termination codes) def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ filling = self.get_current_state() in [ ProtocolState.FILL, ProtocolState.UNKNOWN ] log.debug("_got_chunk:\n%s", chunk) sample_dict = self._extract_sample( PPSDNSampleDataParticle, PPSDNSampleDataParticle.regex_compiled(), chunk, timestamp, publish=filling) if sample_dict and self.get_current_state() != ProtocolState.UNKNOWN: self._linebuf = '' self._promptbuf = '' self._protocol_fsm.on_event( ProtocolEvent.PUMP_STATUS, PPSDNSampleDataParticle.regex_compiled().search(chunk)) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add( Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Flush Volume", description= "Amount of sea water to flush prior to taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Flush Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Flush Minimum Flow", description= "If the flow rate falls below this value the flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Fill Volume", description= "Amount of seawater to run through the collection filter (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Fill Flow Rate", description="Flow rate during sampling: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Fill Minimum Flow", description= "If the flow rate falls below this value the fill will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Clear Volume", description= "Amount of sea water to flush the home port after taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Clear Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Clear Minimum Flow", description= "If the flow rate falls below this value the reverse flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)
class McLaneProtocol(CommandResponseInstrumentProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ # __metaclass__ = get_logging_metaclass(log_level='debug') def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event) # Build protocol state machine. self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT) # Add event handlers for protocol state machine. handlers = { ProtocolState.UNKNOWN: [ (ProtocolEvent.ENTER, self._handler_unknown_enter), (ProtocolEvent.DISCOVER, self._handler_unknown_discover), ], ProtocolState.COMMAND: [ (ProtocolEvent.ENTER, self._handler_command_enter), (ProtocolEvent.INIT_PARAMS, self._handler_command_init_params), (ProtocolEvent.START_DIRECT, self._handler_command_start_direct), (ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock), (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_command_acquire), # (ProtocolEvent.ACQUIRE_STATUS, self._handler_command_status), (ProtocolEvent.CLEAR, self._handler_command_clear), (ProtocolEvent.GET, self._handler_get), (ProtocolEvent.SET, self._handler_command_set), ], ProtocolState.FLUSH: [ (ProtocolEvent.ENTER, self._handler_flush_enter), (ProtocolEvent.FLUSH, self._handler_flush_flush), (ProtocolEvent.PUMP_STATUS, self._handler_flush_pump_status), (ProtocolEvent.INSTRUMENT_FAILURE, self._handler_all_failure), ], ProtocolState.FILL: [ (ProtocolEvent.ENTER, self._handler_fill_enter), (ProtocolEvent.FILL, self._handler_fill_fill), (ProtocolEvent.PUMP_STATUS, self._handler_fill_pump_status), (ProtocolEvent.INSTRUMENT_FAILURE, self._handler_all_failure), ], ProtocolState.CLEAR: [ (ProtocolEvent.ENTER, self._handler_clear_enter), (ProtocolEvent.CLEAR, self._handler_clear_clear), (ProtocolEvent.PUMP_STATUS, self._handler_clear_pump_status), (ProtocolEvent.INSTRUMENT_FAILURE, self._handler_all_failure), ], ProtocolState.RECOVERY: [ (ProtocolEvent.ENTER, self._handler_recovery_enter), ], ProtocolState.DIRECT_ACCESS: [ (ProtocolEvent.ENTER, self._handler_direct_access_enter), (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct), (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct), ], } for state in handlers: for event, handler in handlers[state]: self._protocol_fsm.add_handler(state, event, handler) # Add build handlers for device commands - we are only using simple commands for cmd in McLaneCommand.list(): self._add_build_handler(cmd, self._build_command) # Add response handlers for device commands. # self._add_response_handler(McLaneCommand.BATTERY, self._parse_battery_response) # self._add_response_handler(McLaneCommand.CLOCK, self._parse_clock_response) # self._add_response_handler(McLaneCommand.PORT, self._parse_port_response) # Construct the parameter dictionary containing device parameters, # current parameter values, and set formatting functions. self._build_param_dict() self._build_command_dict() self._build_driver_dict() self._chunker = StringChunker(McLaneProtocol.sieve_function) self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC) # Start state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) self._sent_cmds = None # TODO - reset next_port on mechanical refresh of the PPS filters - how is the driver notified? # TODO - need to persist state for next_port to save driver restart self.next_port = 1 # next available port self._second_attempt = False @staticmethod def sieve_function(raw_data): """ The method that splits samples and status :param raw_data: raw instrument data """ matchers = [] return_list = [] matchers.append(McLaneSampleDataParticle.regex_compiled()) for matcher in matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _filter_capabilities(self, events): """ Return a list of currently available capabilities. """ return [x for x in events if Capability.has(x)] ######################################################################## # implement virtual methods from base class. ######################################################################## def _set_params(self, *args, **kwargs): """ Issue commands to the instrument to set various parameters. If startup is set to true that means we are setting startup values and immutable parameters can be set. Otherwise only READ_WRITE parameters can be set. must be overloaded in derived classes @param params dictionary containing parameter name and value pairs @param startup flag - true indicates initializing, false otherwise """ params = args[0] # check for attempt to set readonly parameters (read-only or immutable set outside startup) self._verify_not_readonly(*args, **kwargs) old_config = self._param_dict.get_config() for (key, val) in params.iteritems(): log.debug("KEY = " + str(key) + " VALUE = " + str(val)) self._param_dict.set_value(key, val) new_config = self._param_dict.get_config() log.debug('new config: %s\nold config: %s', new_config, old_config) # check for parameter change if not dict_equal(old_config, new_config): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def apply_startup_params(self): """ Apply startup parameters """ # fn = "apply_startup_params" # config = self.get_startup_config() # log.debug("%s: startup config = %s", fn, config) # # for param in Parameter.list(): # if param in config: # self._param_dict.set_value(param, config[param]) # # log.debug("%s: new parameters", fn) # for x in config: # log.debug(" parameter %s: %s", x, config[x]) if self.get_current_state() != DriverProtocolState.COMMAND: raise InstrumentProtocolException('cannot set parameters outside command state') self._set_params(self.get_startup_config(), True) ######################################################################## # Instrument commands. ######################################################################## def _do_cmd_resp(self, cmd, *args, **kwargs): """ Perform a command-response on the device. Overrides the base class so it will return the regular expression groups without concatenating them into a string. @param cmd The command to execute. @param args positional arguments to pass to the build handler. @param write_delay kwarg for the amount of delay in seconds to pause between each character. If none supplied, the DEFAULT_WRITE_DELAY value will be used. @param timeout optional wakeup and command timeout via kwargs. @param response_regex kwarg with a compiled regex for the response to match. Groups that match will be returned as a tuple. @retval response The parsed response result. @raises InstrumentTimeoutException if the response did not occur in time. @raises InstrumentProtocolException if command could not be built or if response was not recognized. """ # Get timeout and initialize response. timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT) response_regex = kwargs.get('response_regex', None) # required argument write_delay = INTER_CHARACTER_DELAY retval = None if not response_regex: raise InstrumentProtocolException('missing required keyword argument "response_regex"') if response_regex and not isinstance(response_regex, RE_PATTERN): raise InstrumentProtocolException('Response regex is not a compiled pattern!') # Get the build handler. build_handler = self._build_handlers.get(cmd, None) if not build_handler: raise InstrumentProtocolException('Cannot build command: %s' % cmd) cmd_line = build_handler(cmd, *args) # Wakeup the device, pass up exception if timeout prompt = self._wakeup(timeout) # Clear line and prompt buffers for result. self._linebuf = '' self._promptbuf = '' # Send command. log.debug('_do_cmd_resp: %s, timeout=%s, write_delay=%s, response_regex=%s', repr(cmd_line), timeout, write_delay, response_regex) for char in cmd_line: self._connection.send(char) time.sleep(write_delay) # Wait for the prompt, prepare result and return, timeout exception return self._get_response(timeout, response_regex=response_regex) def _do_cmd_home(self): """ Move valve to the home port @retval True if successful, False if unable to return home """ func = '_do_cmd_home' port = int(self._do_cmd_resp(McLaneCommand.PORT, response_regex=McLaneResponse.PORT)[0]) if port != 0: self._do_cmd_resp(McLaneCommand.HOME, response_regex=McLaneResponse.HOME, timeout=Timeout.HOME) port = int(self._do_cmd_resp(McLaneCommand.PORT, response_regex=McLaneResponse.PORT)[0]) if port != 0: log.error('Unable to return to home port') return False return True def _do_cmd_flush(self, *args, **kwargs): """ Flush the home port in preparation for collecting a sample. This clears the intake port so that the sample taken will be new. This only starts the flush. The remainder of the flush is monitored by got_chunk. """ flush_volume = self._param_dict.get(Parameter.FLUSH_VOLUME) flush_flowrate = self._param_dict.get(Parameter.FLUSH_FLOWRATE) flush_minflow = self._param_dict.get(Parameter.FLUSH_MINFLOW) if not self._do_cmd_home(): self._async_raise_fsm_event(ProtocolEvent.INSTRUMENT_FAILURE) self._do_cmd_no_resp(McLaneCommand.FORWARD, flush_volume, flush_flowrate, flush_minflow) def _do_cmd_fill(self, *args, **kwargs): """ Fill the sample at the next available port """ fill_volume = self._param_dict.get(Parameter.FILL_VOLUME) fill_flowrate = self._param_dict.get(Parameter.FILL_FLOWRATE) fill_minflow = self._param_dict.get(Parameter.FILL_MINFLOW) reply = self._do_cmd_resp(McLaneCommand.PORT, self.next_port, response_regex=McLaneResponse.PORT) self.next_port += 1 # succeed or fail, we can't use this port again # TODO - commit next_port to the agent for persistent data store self._do_cmd_no_resp(McLaneCommand.FORWARD, fill_volume, fill_flowrate, fill_minflow) def _do_cmd_clear(self, *args, **kwargs): """ Clear the home port """ self._do_cmd_home() clear_volume = self._param_dict.get(Parameter.CLEAR_VOLUME) clear_flowrate = self._param_dict.get(Parameter.CLEAR_FLOWRATE) clear_minflow = self._param_dict.get(Parameter.CLEAR_MINFLOW) self._do_cmd_no_resp(McLaneCommand.REVERSE, clear_volume, clear_flowrate, clear_minflow) ######################################################################## # Generic handlers. ######################################################################## def _handler_pass(self, *args, **kwargs): pass def _handler_all_failure(self, *args, **kwargs): next_state = ProtocolState.RECOVERY result = [] log.error('Instrument failure detected. Entering recovery mode.') return next_state, (next_state, result) ######################################################################## # Unknown handlers. ######################################################################## def _handler_unknown_enter(self, *args, **kwargs): """ Enter unknown state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) # TODO - read persistent data (next port) def _handler_unknown_discover(self, *args, **kwargs): """ Discover current state; can only be COMMAND (instrument has no AUTOSAMPLE mode). @retval next_state, next_state """ next_state = ProtocolState.COMMAND result = [] # force to command mode, this instrument has no autosample mode return next_state, (next_state, result) ######################################################################## # Flush ######################################################################## def _handler_flush_enter(self, *args, **kwargs): """ Enter the flush state. Trigger FLUSH event. """ self._second_attempt = False self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._async_raise_fsm_event(ProtocolEvent.FLUSH) def _handler_flush_flush(self, *args, **kwargs): """ Begin flushing the home port. Subsequent flushing will be monitored and sent to the flush_pump_status handler. """ next_state = None result = [] # 2. Set to home port # 3. flush intake (home port) # 4. wait 30 seconds # 1. Get next available port (if no available port, bail) self._do_cmd_flush() return next_state, (next_state, result) def _handler_flush_pump_status(self, *args, **kwargs): """ Manage pump status update during flush. Status updates indicate continued pumping, Result updates indicate completion of command. Check the termination code for success. @args match object containing the regular expression match of the status line. """ match = args[0] pump_status = match.group('status') code = int(match.group('code')) next_state = None result = [] if pump_status == 'Result': if code == TerminationCodeEnum.SUDDEN_FLOW_OBSTRUCTION: log.info('Encountered obstruction during flush, attempting to clear') self._async_raise_fsm_event(ProtocolEvent.CLEAR) else: next_state = ProtocolState.FILL return next_state, (next_state, result) def _handler_flush_clear(self, *args, **kwargs): """ Attempt to clear home port after stoppage has occurred during flush. This is only performed once. On the second stoppage, the driver will enter recovery mode. """ next_state = None result = [] if self._second_attempt: next_state = ProtocolState.RECOVERY else: self._second_attempt = True self._do_cmd_clear() return next_state, (next_state, result) ######################################################################## # Fill ######################################################################## def _handler_fill_enter(self, *args, **kwargs): """ Enter the fill state. Trigger FILL event. """ self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._async_raise_fsm_event(ProtocolEvent.FILL) def _handler_fill_fill(self, *args, **kwargs): """ Send the fill command and process the first response """ next_state = None result = [] log.debug('Entering PHIL PHIL') # 5. switch to collection port (next available) # 6. collect sample (4000 ml) # 7. wait 2 minutes if self.next_port > NUM_PORTS: log.error('Unable to collect RAS sample - %d containers full', NUM_PORTS) next_state = ProtocolState.COMMAND else: self._do_cmd_fill() return next_state, (next_state, result) def _handler_fill_pump_status(self, *args, **kwargs): """ Process pump status updates during filter collection. """ next_state = None result = [] match = args[0] pump_status = match.group('status') code = int(match.group('code')) if pump_status == 'Result': if code != TerminationCodeEnum.VOLUME_REACHED: next_state = ProtocolState.RECOVERY result = 'unable to fill - possible obstruction - will attempt to clear' else: next_state = ProtocolState.CLEAR # all done result = 'volume reached' # if pump_status == 'Status': # TODO - check for bag rupture (> 93% flow rate near end of sample collect- RAS only) return next_state, (next_state, result) ######################################################################## # Clear ######################################################################## def _handler_clear_enter(self, *args, **kwargs): """ Enter the clear state. Trigger the CLEAR event. """ self._second_attempt = False self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._async_raise_fsm_event(ProtocolEvent.CLEAR) def _handler_clear_clear(self, *args, **kwargs): """ Send the clear command. If there is an obstruction trigger a FLUSH, otherwise place driver in RECOVERY mode. """ next_state = None result = [] # 8. return to home port # 9. reverse flush 75 ml to pump water from exhaust line through intake line self._do_cmd_clear() return next_state, (next_state, result) def _handler_clear_pump_status(self, *args, **kwargs): """ Parse pump status during clear action. """ next_state = None result = [] match = args[0] pump_status = match.group('status') code = int(match.group('code')) if pump_status == 'Result': if code != TerminationCodeEnum.VOLUME_REACHED: result = 'Encountered obstruction during clear. Attempting flush...' log.error(result) self._async_raise_fsm_event(ProtocolEvent.FLUSH) else: next_state = ProtocolState.COMMAND result = 'clear successful' # if Status, nothing to do return next_state, (next_state, result) def _handler_clear_flush(self, *args, **kwargs): """ Attempt to recover from failed attempt to clear by flushing home port. Only try once. """ next_state = None result = [] log.info('Attempting to flush main port during clear') if self._second_attempt: next_state = ProtocolState.RECOVERY result = 'unable to flush main port during clear' else: self._second_attempt = True self._do_cmd_flush() result = 'attempting to flush main port a second time' return next_state, (next_state, result) ######################################################################## # Command handlers. # just implemented to make DA possible, instrument has no actual command mode ######################################################################## def _handler_command_enter(self, *args, **kwargs): """ Enter command state. """ # Command device to update parameters and send a config change event if needed. self._update_params() self._protocol_fsm.on_event(ProtocolEvent.INIT_PARAMS) # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_command_init_params(self, *args, **kwargs): """ Setup initial parameters. """ next_state = None result = self._init_params() return next_state, (next_state, result) def _handler_command_set(self, *args, **kwargs): """ Set instrument parameters """ next_state = None result = [] startup = False try: params = args[0] except IndexError: raise InstrumentParameterException('set command requires a parameter dictionary.') try: startup = args[1] except IndexError: pass if not isinstance(params, dict): raise InstrumentParameterException('set parameters is not a dictionary') self._set_params(params, startup) return next_state, (next_state, result) def _handler_command_start_direct(self, *args, **kwargs): """ Start direct access. """ next_state = ProtocolState.DIRECT_ACCESS result = [] return next_state, (next_state, result) ######################################################################## # Recovery handlers. ######################################################################## # TODO - not sure how to determine how to exit from this state. Probably requires a driver reset. def _handler_recovery_enter(self, *args, **kwargs): """ Error recovery mode. The instrument failed to respond to a command and now requires the user to perform diagnostics and correct before proceeding. """ self._driver_event(DriverAsyncEvent.STATE_CHANGE) ######################################################################## # Direct access handlers. ######################################################################## def _handler_direct_access_enter(self, *args, **kwargs): """ Enter direct access state. """ # Tell driver superclass to send a state change event. # Superclass will query the state. self._driver_event(DriverAsyncEvent.STATE_CHANGE) self._sent_cmds = [] def _handler_direct_access_execute_direct(self, data): next_state = None result = [] self._do_cmd_direct(data) return next_state, (next_state, result) def _handler_direct_access_stop_direct(self, *args, **kwargs): next_state = ProtocolState.COMMAND result = [] return next_state, (next_state, result) ######################################################################## # general handlers. ######################################################################## def get_timestamp_delayed(self, fmt, delay=0): """ Return a formatted date string of the current utc time, but the string return is delayed until the next second transition. Formatting: http://docs.python.org/library/time.html#time.strftime :param fmt: strftime() format string :param delay: optional time to wait before getting timestamp :return: formatted date string :raise ValueError if format is None """ if not fmt: raise ValueError now = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) time.sleep((1e6 - now.microsecond) / 1e6) now = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) return now.strftime(fmt) def _handler_sync_clock(self, *args, **kwargs): """ sync clock close to a second edge @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)). @throws InstrumentTimeoutException if device respond correctly. @throws InstrumentProtocolException if command could not be built or misunderstood. """ cmd_len = len('clock 03/20/2014 17:14:55' + NEWLINE) delay = cmd_len * INTER_CHARACTER_DELAY time_format = "%m/%d/%Y %H:%M:%S" str_val = self.get_timestamp_delayed(time_format, delay) # str_val = time.strftime(time_format, time.gmtime(time.time() + self._clock_set_offset)) log.debug("Setting instrument clock to '%s'", str_val) ras_time = self._do_cmd_resp(McLaneCommand.CLOCK, str_val, response_regex=McLaneResponse.READY)[0] return None, (None, {'time': ras_time}) def _handler_command_acquire(self, *args, **kwargs): next_state = ProtocolState.FLUSH result = [] self._handler_sync_clock() return next_state, (next_state, result) # def _handler_command_status(self, *args, **kwargs): # # get the following: # # - VERSION # # - CAPACITY (pump flow) # # - BATTERY # # - CODES (termination codes) # # - COPYRIGHT (termination codes) # return None, None def _handler_command_clear(self, *args, **kwargs): next_state = ProtocolState.CLEAR result = [] return next_state, (next_state, result) ######################################################################## # Private helpers. ######################################################################## def _wakeup(self, wakeup_timeout=10, response_timeout=3): """ Over-written because waking this instrument up is a multi-step process with two different requests required @param wakeup_timeout The timeout to wake the device. @param response_timeout The time to look for response to a wakeup attempt. @throw InstrumentTimeoutException if the device could not be woken. """ sleep_time = .1 command = McLaneCommand.GO # Grab start time for overall wakeup timeout. starttime = time.time() while True: # Clear the prompt buffer. log.debug("_wakeup: clearing promptbuf: %s", self._promptbuf) self._promptbuf = '' # Send a command and wait delay amount for response. log.debug('_wakeup: Sending command %s, delay=%s', command.encode("hex"), response_timeout) for char in command: self._connection.send(char) time.sleep(INTER_CHARACTER_DELAY) sleep_amount = 0 while True: time.sleep(sleep_time) if self._promptbuf.find(Prompt.COMMAND_INPUT) != -1: # instrument is awake log.debug('_wakeup: got command input prompt %s', Prompt.COMMAND_INPUT) # add inter-character delay which _do_cmd_resp() incorrectly doesn't add to # the start of a transmission time.sleep(INTER_CHARACTER_DELAY) return Prompt.COMMAND_INPUT if self._promptbuf.find(Prompt.ENTER_CTRL_C) != -1: command = McLaneCommand.CONTROL_C break if self._promptbuf.find(Prompt.PERIOD) == 0: command = McLaneCommand.CONTROL_C break sleep_amount += sleep_time if sleep_amount >= response_timeout: log.debug("_wakeup: expected response not received, buffer=%s", self._promptbuf) break if time.time() > starttime + wakeup_timeout: raise InstrumentTimeoutException( "_wakeup(): instrument failed to wakeup in %d seconds time" % wakeup_timeout) def _build_command(self, cmd, *args): return cmd + ' ' + ' '.join([str(x) for x in args]) + NEWLINE def _build_driver_dict(self): """ Populate the driver dictionary with options """ self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False) def _build_command_dict(self): """ Populate the command dictionary with command. """ self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="Synchronize Clock") self._cmd_dict.add(Capability.DISCOVER, display_name='Discover') def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=150, units='mL', startup_param=True, display_name="flush_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="flush_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="flush_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=4000, units='mL', startup_param=True, display_name="fill_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="fill_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="fill_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL', startup_param=True, display_name="clear_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=100, units='mL/min', startup_param=True, display_name="clear_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=75, units='mL/min', startup_param=True, display_name="clear_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) def _update_params(self): """
class Protocol(SamiProtocol): """ Instrument protocol class Subclasses SamiProtocol and CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. SamiProtocol.__init__(self, prompts, newline, driver_event) ## Continue building protocol state machine from SamiProtocol self._protocol_fsm.add_handler(ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler(ProtocolState.SCHEDULED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) self._protocol_fsm.add_handler(ProtocolState.POLLED_SAMPLE, ProtocolEvent.SUCCESS, self._handler_sample_success) self._protocol_fsm.add_handler(ProtocolState.POLLED_SAMPLE, ProtocolEvent.TIMEOUT, self._handler_sample_timeout) # State state machine in UNKNOWN state. self._protocol_fsm.start(ProtocolState.UNKNOWN) # build the chunker bot self._chunker = StringChunker(Protocol.sieve_function) @staticmethod def sieve_function(raw_data): """ The method that splits samples """ return_list = [] sieve_matchers = [ REGULAR_STATUS_REGEX_MATCHER, CONTROL_RECORD_REGEX_MATCHER, SAMI_SAMPLE_REGEX_MATCHER, CONFIGURATION_REGEX_MATCHER, ERROR_REGEX_MATCHER ] for matcher in sieve_matchers: for match in matcher.finditer(raw_data): return_list.append((match.start(), match.end())) return return_list def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ self._extract_sample(SamiRegularStatusDataParticle, REGULAR_STATUS_REGEX_MATCHER, chunk, timestamp) self._extract_sample(SamiControlRecordDataParticle, CONTROL_RECORD_REGEX_MATCHER, chunk, timestamp) self._extract_sample(PhsenSamiSampleDataParticle, SAMI_SAMPLE_REGEX_MATCHER, chunk, timestamp) self._extract_sample(PhsenConfigDataParticle, CONFIGURATION_REGEX_MATCHER, chunk, timestamp) ######################################################################### ## General (for POLLED and SCHEDULED states) Sample handlers. ######################################################################### def _handler_sample_success(self, *args, **kwargs): next_state = None result = None return (next_state, result) def _handler_sample_timeout(self, ): next_state = None result = None return (next_state, result) #################################################################### # Build Parameter dictionary #################################################################### def _build_param_dict(self): """ Populate the parameter dictionary with parameters. For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() self._param_dict.add(Parameter.LAUNCH_TIME, CONFIGURATION_REGEX, lambda match: int(match.group(1), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='launch time') self._param_dict.add(Parameter.START_TIME_FROM_LAUNCH, CONFIGURATION_REGEX, lambda match: int(match.group(2), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02C7EA00, visibility=ParameterDictVisibility.READ_ONLY, display_name='start time after launch time') self._param_dict.add(Parameter.STOP_TIME_FROM_START, CONFIGURATION_REGEX, lambda match: int(match.group(3), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01E13380, visibility=ParameterDictVisibility.READ_ONLY, display_name='stop time after start time') self._param_dict.add(Parameter.MODE_BITS, CONFIGURATION_REGEX, lambda match: int(match.group(4), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0A, visibility=ParameterDictVisibility.READ_ONLY, display_name='mode bits (set to 00001010)') self._param_dict.add(Parameter.SAMI_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(5), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami sample interval') self._param_dict.add(Parameter.SAMI_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(6), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x04, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami driver version') self._param_dict.add(Parameter.SAMI_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(7), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami parameter pointer') self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(8), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 sample interval') self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(9), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 driver version') self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(10), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0B, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 parameter pointer') self._param_dict.add(Parameter.DEVICE2_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(11), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 sample interval') self._param_dict.add(Parameter.DEVICE2_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(12), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 driver version') self._param_dict.add(Parameter.DEVICE2_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(13), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 parameter pointer') self._param_dict.add(Parameter.DEVICE3_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(14), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 sample interval') self._param_dict.add(Parameter.DEVICE3_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(15), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 driver version') self._param_dict.add(Parameter.DEVICE3_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(16), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 parameter pointer') self._param_dict.add(Parameter.PRESTART_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(17), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart sample interval') self._param_dict.add(Parameter.PRESTART_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(18), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart driver version') self._param_dict.add(Parameter.PRESTART_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(19), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart parameter pointer') self._param_dict.add(Parameter.GLOBAL_CONFIGURATION, CONFIGURATION_REGEX, lambda match: int(match.group(20), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='global bits (set to 00000111)') self._param_dict.add(Parameter.NUMBER_SAMPLES_AVERAGED, CONFIGURATION_REGEX, lambda match: int(match.group(21), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_FLUSHES, CONFIGURATION_REGEX, lambda match: int(match.group(22), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(23), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_FLUSH, CONFIGURATION_REGEX, lambda match: int(match.group(24), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_REAGENT_PUMPS, CONFIGURATION_REGEX, lambda match: int(match.group(25), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.VALVE_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(26), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_ON_IND, CONFIGURATION_REGEX, lambda match: int(match.group(27), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PV_OFF_IND, CONFIGURATION_REGEX, lambda match: int(match.group(28), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_BLANKS, CONFIGURATION_REGEX, lambda match: int(match.group(29), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_MEASURE_T, CONFIGURATION_REGEX, lambda match: int(match.group(30), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.PUMP_OFF_TO_MEASURE, CONFIGURATION_REGEX, lambda match: int(match.group(31), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.MEASURE_TO_PUMP_ON, CONFIGURATION_REGEX, lambda match: int(match.group(32), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.NUMBER_MEASUREMENTS, CONFIGURATION_REGEX, lambda match: int(match.group(33), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='') self._param_dict.add(Parameter.SALINITY_DELAY, CONFIGURATION_REGEX, lambda match: int(match.group(34), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of samples averaged', description='')
class Protocol(McLaneProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. McLaneProtocol.__init__(self, prompts, newline, driver_event) # TODO - reset next_port on mechanical refresh of the RAS bags - how is the driver notified? # TODO - need to persist state for next_port to save driver restart self.next_port = 1 # next available port # TODO - on init, perform the following commands: # - VERSION # - CAPACITY (pump flow) # - BATTERY # - CODES (termination codes) # - COPYRIGHT (termination codes) def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ filling = self.get_current_state() == ProtocolState.FILL log.debug("_got_chunk:\n%s", chunk) sample_dict = self._extract_sample(RASFLSampleDataParticle, RASFLSampleDataParticle.regex_compiled(), chunk, timestamp, publish=filling) if sample_dict: self._linebuf = '' self._promptbuf = '' self._protocol_fsm.on_event(ProtocolEvent.PUMP_STATUS, RASFLSampleDataParticle.regex_compiled().search(chunk)) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units='mL', startup_param=True, display_name="flush_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units='mL/min', startup_param=True, display_name="flush_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units='mL/min', startup_param=True, display_name="flush_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units='mL', startup_param=True, display_name="fill_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units='mL/min', startup_param=True, display_name="fill_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units='mL/min', startup_param=True, display_name="fill_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units='mL', startup_param=True, display_name="clear_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units='mL/min', startup_param=True, display_name="clear_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units='mL/min', startup_param=True, display_name="clear_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)
def _build_param_dict(self): """ For each parameter key, add match stirng, match lambda function, and value formatting function for set commands. """ # Add parameter handlers to parameter dict. self._param_dict = ProtocolParameterDict() ### example configuration string # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \ # '000000000D000000000D000000000D07' + \ # '1020FF54181C01003814' + \ # '000000000000000000000000000000000000000000000000000' + \ # '000000000000000000000000000000000000000000000000000' + \ # '0000000000000000000000000000' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \ # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE # ### self._param_dict.add(Parameter.LAUNCH_TIME, CONFIGURATION_REGEX, lambda match: int(match.group(1), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='launch time') self._param_dict.add(Parameter.START_TIME_FROM_LAUNCH, CONFIGURATION_REGEX, lambda match: int(match.group(2), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02C7EA00, visibility=ParameterDictVisibility.READ_ONLY, display_name='start time after launch time') self._param_dict.add(Parameter.STOP_TIME_FROM_START, CONFIGURATION_REGEX, lambda match: int(match.group(3), 16), lambda x: self._int_to_hexstring(x, 8), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01E13380, visibility=ParameterDictVisibility.READ_ONLY, display_name='stop time after start time') self._param_dict.add(Parameter.MODE_BITS, CONFIGURATION_REGEX, lambda match: int(match.group(4), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0A, visibility=ParameterDictVisibility.READ_ONLY, display_name='mode bits (set to 00001010)') self._param_dict.add(Parameter.SAMI_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(5), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami sample interval') self._param_dict.add(Parameter.SAMI_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(6), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x04, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami driver version') self._param_dict.add(Parameter.SAMI_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(7), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x02, visibility=ParameterDictVisibility.READ_ONLY, display_name='sami parameter pointer') self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(8), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000E10, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 sample interval') self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(9), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 driver version') self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(10), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0B, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 1 parameter pointer') self._param_dict.add(Parameter.DEVICE2_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(11), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 sample interval') self._param_dict.add(Parameter.DEVICE2_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(12), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 driver version') self._param_dict.add(Parameter.DEVICE2_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(13), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 2 parameter pointer') self._param_dict.add(Parameter.DEVICE3_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(14), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 sample interval') self._param_dict.add(Parameter.DEVICE3_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(15), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 driver version') self._param_dict.add(Parameter.DEVICE3_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(16), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='device 3 parameter pointer') self._param_dict.add(Parameter.PRESTART_SAMPLE_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(17), 16), lambda x: self._int_to_hexstring(x, 6), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x000000, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart sample interval') self._param_dict.add(Parameter.PRESTART_DRIVER_VERSION, CONFIGURATION_REGEX, lambda match: int(match.group(18), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart driver version') self._param_dict.add(Parameter.PRESTART_PARAMS_POINTER, CONFIGURATION_REGEX, lambda match: int(match.group(19), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x0D, visibility=ParameterDictVisibility.READ_ONLY, display_name='prestart parameter pointer') self._param_dict.add(Parameter.GLOBAL_CONFIGURATION, CONFIGURATION_REGEX, lambda match: int(match.group(20), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='global bits (set to 00000111)') self._param_dict.add(Parameter.PUMP_PULSE, CONFIGURATION_REGEX, lambda match: int(match.group(21), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x10, visibility=ParameterDictVisibility.READ_ONLY, display_name='pump pulse duration') self._param_dict.add(Parameter.PUMP_DURATION, CONFIGURATION_REGEX, lambda match: int(match.group(22), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x20, visibility=ParameterDictVisibility.READ_ONLY, display_name='pump measurement duration') self._param_dict.add(Parameter.SAMPLES_PER_MEASUREMENT, CONFIGURATION_REGEX, lambda match: int(match.group(23), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0xFF, visibility=ParameterDictVisibility.READ_ONLY, display_name='samples per measurement') self._param_dict.add(Parameter.CYCLES_BETWEEN_BLANKS, CONFIGURATION_REGEX, lambda match: int(match.group(24), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0xA8, visibility=ParameterDictVisibility.READ_ONLY, display_name='cycles between blanks') self._param_dict.add(Parameter.NUMBER_REAGENT_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(25), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x18, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of reagent cycles') self._param_dict.add(Parameter.NUMBER_BLANK_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(26), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x1C, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of blank cycles') self._param_dict.add(Parameter.FLUSH_PUMP_INTERVAL, CONFIGURATION_REGEX, lambda match: int(match.group(27), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x01, visibility=ParameterDictVisibility.READ_ONLY, display_name='flush pump interval') self._param_dict.add(Parameter.BIT_SWITCHES, CONFIGURATION_REGEX, lambda match: int(match.group(28), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x00, visibility=ParameterDictVisibility.READ_ONLY, display_name='bit switches') self._param_dict.add(Parameter.NUMBER_EXTRA_PUMP_CYCLES, CONFIGURATION_REGEX, lambda match: int(match.group(29), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x38, visibility=ParameterDictVisibility.READ_ONLY, display_name='number of extra pump cycles') self._param_dict.add(Parameter.EXTERNAL_PUMP_SETTINGS, CONFIGURATION_REGEX, lambda match: int(match.group(30), 16), lambda x: self._int_to_hexstring(x, 2), type=ParameterDictType.INT, startup_param=False, direct_access=True, default_value=0x14, visibility=ParameterDictVisibility.READ_ONLY, display_name='external pump settings')
class DataSetDriver(object): """ Base class for data set drivers. Provides: - an interface via callback to publish data - an interface via callback to persist driver state - an interface via callback to handle exceptions - an start and stop sampling - a client interface for execute resource Subclasses need to include harvesters and parsers and be specialized to handle the interaction between the two. Configurations should contain keys from the DataSetDriverConfigKey class and should look something like this example (more full documentation in the "Dataset Agent Architecture" page on the OOI wiki): { 'harvester': { 'directory': '/tmp/dsatest', 'pattern': '*.txt', 'frequency': 1, }, 'parser': {} 'driver': { 'records_per_second' 'harvester_polling_interval' 'batched_particle_count' } } """ def __init__(self, config, memento, data_callback, state_callback, exception_callback): self._config = config self._data_callback = data_callback self._state_callback = state_callback self._exception_callback = exception_callback self._memento = memento self._publisher_thread = None self._verify_config() self._param_dict = ProtocolParameterDict() # Updated my set_resource, defaults defined in build_param_dict self._polling_interval = None self._generate_particle_count = None self._particle_count_per_second = None self._build_param_dict() def shutdown(self): self.stop_sampling() def start_sampling(self): """ Start a new thread to monitor for data """ self._start_sampling() self._start_publisher_thread() def stop_sampling(self): """ Stop the sampling thread """ log.debug("Stopping driver now") self._stop_sampling() self._stop_publisher_thread() def _start_sampling(self): raise NotImplementedException( 'virtual methond needs to be specialized') def _stop_sampling(self): raise NotImplementedException( 'virtual methond needs to be specialized') def cmd_dvr(self, cmd, *args, **kwargs): log.warn("DRIVER: cmd_dvr %s", cmd) if not cmd in [ 'execute_resource', 'get_resource_capabilities', 'set_resource', 'get_resource' ]: log.error("Unhandled command: %s", cmd) raise InstrumentStateException("Unhandled command: %s" % cmd) resource_cmd = args[0] if cmd == 'execute_resource': if resource_cmd == DriverEvent.START_AUTOSAMPLE: return (ResourceAgentState.STREAMING, None) elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE: self.stop_sampling() return (ResourceAgentState.COMMAND, None) else: log.error("Unhandled resource command: %s", resource_cmd) raise elif cmd == 'get_resource_capabilities': return self.get_resource_capabilities() elif cmd == 'set_resource': return self.set_resource(*args, **kwargs) elif cmd == 'get_resource': return self.get_resource(*args, **kwargs) def get_resource_capabilities(self, current_state=True, *args, **kwargs): """ Return driver commands and parameters. @param current_state True to retrieve commands available in current state, otherwise reutrn all commands. @retval list of AgentCapability objects representing the drivers capabilities. @raises NotImplementedException if not implemented by subclass. """ res_params = self._param_dict.get_keys() return [[], res_params] def set_resource(self, *args, **kwargs): """ Set the driver parameter """ log.trace("start set_resource") try: params = args[0] except IndexError: raise InstrumentParameterException( 'Set command requires a parameter dict.') log.trace("set_resource: iterate through params: %s", params) for (key, val) in params.iteritems(): if key in [ DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND ]: if not isinstance(val, int): raise InstrumentParameterException( "%s must be an integer" % key) if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]: if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key) if val <= 0: raise InstrumentParameterException("%s must be > 0" % key) self._param_dict.set_value(key, val) # Set the driver parameters self._generate_particle_count = self._param_dict.get( DriverParameter.BATCHED_PARTICLE_COUNT) self._particle_count_per_second = self._param_dict.get( DriverParameter.RECORDS_PER_SECOND) self._polling_interval = self._param_dict.get( DriverParameter.PUBLISHER_POLLING_INTERVAL) log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second, self._generate_particle_count) def get_resource(self, *args, **kwargs): """ Get driver parameter """ result = {} try: params = args[0] except IndexError: raise InstrumentParameterException( 'Set command requires a parameter list.') # If all params requested, retrieve config. if params == DriverParameter.ALL: result = self._param_dict.get_config() # If not all params, confirm a list or tuple of params to retrieve. # Raise if not a list or tuple. # Retrieve each key in the list, raise if any are invalid. else: if not isinstance(params, (list, tuple)): raise InstrumentParameterException( 'Get argument not a list or tuple.') result = {} for key in params: try: val = self._param_dict.get(key) result[key] = val except KeyError: raise InstrumentParameterException( ('%s is not a valid parameter.' % key)) return result def _verify_config(self): """ virtual method to verify the supplied driver configuration is value. Must be overloaded in sub classes. raises an ConfigurationException when a configuration error is detected. """ raise NotImplementedException( 'virtual methond needs to be specialized') def _build_param_dict(self): """ Setup three common driver parameters """ self._param_dict.add_parameter( Parameter(DriverParameter.RECORDS_PER_SECOND, int, value=60, type=ParameterDictType.INT, display_name="Records Per Second", description="Number of records to process per second")) self._param_dict.add_parameter( Parameter( DriverParameter.PUBLISHER_POLLING_INTERVAL, float, value=1, type=ParameterDictType.FLOAT, display_name="Harvester Polling Interval", description= "Duration in minutes to wait before checking for new files.")) self._param_dict.add_parameter( Parameter( DriverParameter.BATCHED_PARTICLE_COUNT, int, value=1, type=ParameterDictType.INT, display_name="Batched Particle Count", description= "Number of particles to batch before sending to the agent")) config = self._config.get(DataSourceConfigKey.DRIVER, {}) log.debug("set_resource on startup with: %s", config) self.set_resource(config) def _start_publisher_thread(self): self._publisher_thread = gevent.spawn(self._publisher_loop) self._publisher_shutdown = False def _stop_publisher_thread(self): log.debug("Signal shutdown") self._publisher_shutdown = True if self._publisher_thread: self._publisher_thread.kill(block=False) log.debug("shutdown complete") def _publisher_loop(self): """ Main loop to listen for new files to parse. Parse them and move on. """ log.info("Starting main publishing loop") try: while (not self._publisher_shutdown): self._poll() gevent.sleep(self._polling_interval) except Exception as e: log.error("Exception in publisher thread: %s", e) self._exception_callback(e) log.debug("publisher thread detected shutdown request") def _poll(self): raise NotImplementedException( 'virtual methond needs to be specialized') def _new_file_exception(self): raise NotImplementedException( 'virtual methond needs to be specialized')
class Protocol(WorkhorseProtocol): def __init__(self, prompts, newline, driver_event, connections=None): """ Constructor. @param prompts Enum class containing possible device prompts used for command response logic. @param newline The device newline. @driver_event The callback for asynchronous driver events. """ if not type(connections) is list: raise InstrumentProtocolException('Unable to instantiate multi connection protocol without connection list') self._param_dict2 = ProtocolParameterDict() # Construct superclass. WorkhorseProtocol.__init__(self, prompts, newline, driver_event) # Create multiple connection versions of the pieces of protocol involving data to/from the instrument self._linebuf = {connection: '' for connection in connections} self._promptbuf = {connection: '' for connection in connections} self._last_data_timestamp = {connection: None for connection in connections} self.connections = {connection: None for connection in connections} self.chunkers = {connection: StringChunker(self.sieve_function) for connection in connections} def _get_response(self, timeout=10, expected_prompt=None, response_regex=None, connection=None): """ Overridden to handle multiple port agent connections """ if connection is None: raise InstrumentProtocolException('_get_response: no connection supplied!') # Grab time for timeout and wait for prompt. end_time = time.time() + timeout if response_regex and not isinstance(response_regex, RE_PATTERN): raise InstrumentProtocolException('Response regex is not a compiled pattern!') if expected_prompt and response_regex: raise InstrumentProtocolException('Cannot supply both regex and expected prompt!') if expected_prompt is None: prompt_list = self._get_prompts() else: if isinstance(expected_prompt, str): prompt_list = [expected_prompt] else: prompt_list = expected_prompt if response_regex is None: pattern = None else: pattern = response_regex.pattern log.debug('_get_response: timeout=%s, prompt_list=%s, expected_prompt=%r, response_regex=%r, promptbuf=%r', timeout, prompt_list, expected_prompt, pattern, self._promptbuf) while time.time() < end_time: if response_regex: # noinspection PyArgumentList match = response_regex.search(self._linebuf[connection]) if match: return match.groups() else: for item in prompt_list: index = self._promptbuf[connection].find(item) if index >= 0: result = self._promptbuf[connection][0:index + len(item)] return item, result time.sleep(.1) raise InstrumentTimeoutException("in InstrumentProtocol._get_response()") def _do_cmd_resp(self, cmd, *args, **kwargs): """ Overridden to handle multiple port agent connections """ connection = kwargs.get('connection') if connection is None: raise InstrumentProtocolException('_do_cmd_resp: no connection supplied!') # Get timeout and initialize response. timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT) expected_prompt = kwargs.get('expected_prompt', None) response_regex = kwargs.get('response_regex', None) if response_regex and not isinstance(response_regex, RE_PATTERN): raise InstrumentProtocolException('Response regex is not a compiled pattern!') if expected_prompt and response_regex: raise InstrumentProtocolException('Cannot supply both regex and expected prompt!') self._do_cmd_no_resp(cmd, *args, **kwargs) # Wait for the prompt, prepare result and return, timeout exception if response_regex: prompt = "" result_tuple = self._get_response(timeout, connection=connection, response_regex=response_regex, expected_prompt=expected_prompt) result = "".join(result_tuple) else: (prompt, result) = self._get_response(timeout, connection=connection, expected_prompt=expected_prompt) resp_handler = self._response_handlers.get((self.get_current_state(), cmd), self._response_handlers.get(cmd, None)) resp_result = None if callable(resp_handler): resp_result = resp_handler(result, prompt) return resp_result def _send_data(self, data, write_delay=0, connection=None): if connection is None: raise InstrumentProtocolException('_send_data: no connection supplied!') if write_delay == 0: self.connections[connection].send(data) else: for char in data: self.connections[connection].send(char) time.sleep(write_delay) def _do_cmd_no_resp(self, cmd, *args, **kwargs): """ Overridden to handle multiple port agent connections """ connection = kwargs.get('connection') if connection is None: raise InstrumentProtocolException('_do_cmd_no_resp: no connection supplied! %r %r %r' % (cmd, args, kwargs)) timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT) write_delay = kwargs.get('write_delay', DEFAULT_WRITE_DELAY) build_handler = self._build_handlers.get(cmd, None) if not callable(build_handler): log.error('_do_cmd_no_resp: no handler for command: %s' % cmd) raise InstrumentProtocolException(error_code=InstErrorCode.BAD_DRIVER_COMMAND) cmd_line = build_handler(cmd, *args) # Wakeup the device, timeout exception as needed self._wakeup(timeout, connection=connection) # Clear line and prompt buffers for result, then send command. self._linebuf[connection] = '' self._promptbuf[connection] = '' self._send_data(cmd_line, write_delay, connection=connection) def _do_cmd_direct(self, cmd, connection=None): """ Issue an untranslated command to the instrument. No response is handled as a result of the command. @param cmd The high level command to issue """ # Send command. self._send_data(cmd, connection=connection) ######################################################################## # Incoming data (for parsing) callback. ######################################################################## def got_data(self, port_agent_packet, connection=None): """ Called by the instrument connection when data is available. Append line and prompt buffers. Also add data to the chunker and when received call got_chunk to publish results. """ if connection is None: raise InstrumentProtocolException('got_data: no connection supplied!') data_length = port_agent_packet.get_data_length() data = port_agent_packet.get_data() timestamp = port_agent_packet.get_timestamp() log.debug("Got Data: %r %r", connection, data) log.debug("Add Port Agent Timestamp: %r %s", connection, timestamp) if data_length > 0: if self.get_current_state() == DriverProtocolState.DIRECT_ACCESS: self._driver_event(DriverAsyncEvent.DIRECT_ACCESS, data) self.add_to_buffer(data, connection=connection) self.chunkers[connection].add_chunk(data, timestamp) (timestamp, chunk) = self.chunkers[connection].get_next_data() while chunk: self._got_chunk(chunk, timestamp, connection=connection) (timestamp, chunk) = self.chunkers[connection].get_next_data() ######################################################################## # Incoming raw data callback. ######################################################################## def got_raw(self, port_agent_packet, connection=None): """ Called by the port agent client when raw data is available, such as data sent by the driver to the instrument, the instrument responses,etc. """ self.publish_raw(port_agent_packet, connection) def publish_raw(self, port_agent_packet, connection=None): """ Publish raw data @param: port_agent_packet port agent packet containing raw """ if connection == SlaveProtocol.FOURBEAM: particle_class = RawDataParticle else: particle_class = RawDataParticle5 particle = particle_class(port_agent_packet.get_as_dict(), port_timestamp=port_agent_packet.get_timestamp()) if self._driver_event: self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate()) def add_to_buffer(self, data, connection=None): """ Add a chunk of data to the internal data buffers buffers implemented as lifo ring buffer @param data: bytes to add to the buffer """ # Update the line and prompt buffers. self._linebuf[connection] += data self._promptbuf[connection] += data self._last_data_timestamp[connection] = time.time() # If our buffer exceeds the max allowable size then drop the leading # characters on the floor. if len(self._linebuf[connection]) > self._max_buffer_size(): self._linebuf[connection] = self._linebuf[connection][self._max_buffer_size() * -1:] # If our buffer exceeds the max allowable size then drop the leading # characters on the floor. if len(self._promptbuf[connection]) > self._max_buffer_size(): self._promptbuf[connection] = self._promptbuf[connection][self._max_buffer_size() * -1:] log.debug("LINE BUF: %r", self._linebuf[connection][-50:]) log.debug("PROMPT BUF: %r", self._promptbuf[connection][-50:]) ######################################################################## # Wakeup helpers. ######################################################################## def _send_wakeup(self, connection=None): """ Send a wakeup to the device. Overridden by device specific subclasses. """ self.connections[connection].send(NEWLINE) def _wakeup(self, timeout, delay=1, connection=None): """ Clear buffers and send a wakeup command to the instrument @param timeout The timeout to wake the device. @param delay The time to wait between consecutive wakeups. @throw InstrumentTimeoutException if the device could not be woken. """ if connection is None: raise InstrumentProtocolException('_wakeup: no connection supplied!') # Clear the prompt buffer. log.trace("clearing promptbuf: %r", self._promptbuf) self._promptbuf[connection] = '' # Grab time for timeout. starttime = time.time() while True: # Send a line return and wait a sec. log.trace('Sending wakeup. timeout=%s', timeout) self._send_wakeup(connection=connection) time.sleep(delay) log.trace("Prompts: %s", self._get_prompts()) for item in self._get_prompts(): log.trace("buffer: %r", self._promptbuf[connection]) log.trace("find prompt: %r", item) index = self._promptbuf[connection].find(item) log.trace("Got prompt (index: %s): %r ", index, self._promptbuf[connection]) if index >= 0: log.trace('wakeup got prompt: %r', item) return item log.trace("Searched for all prompts") if time.time() > starttime + timeout: raise InstrumentTimeoutException("in _wakeup()") def _build_param_dict(self): # We're going to build two complete sets of ADCP parameters here # one set for the master instrument and one for the slave for param in parameter_regexes: self._param_dict.add(param, parameter_regexes.get(param), parameter_extractors.get(param), parameter_formatters.get(param), type=parameter_types.get(param), display_name=parameter_names.get(param), value_description=parameter_descriptions.get(param), startup_param=parameter_startup.get(param, False), direct_access=parameter_direct.get(param, False), visibility=parameter_visibility.get(param, ParameterDictVisibility.READ_WRITE), default_value=master_parameter_defaults.get(param), units=parameter_units.get(param)) for param in parameter_regexes: # Scheduled events are handled by the master if WorkhorseEngineeringParameter.has(param): continue self._param_dict.add(param + '_5th', r'DONTMATCHMEIMNOTREAL!', parameter_extractors.get(param), parameter_formatters.get(param), type=parameter_types.get(param), display_name=parameter_names.get(param), value_description=parameter_descriptions.get(param), startup_param=parameter_startup.get(param, False), direct_access=parameter_direct.get(param, False), visibility=parameter_visibility.get(param, ParameterDictVisibility.READ_WRITE), default_value=slave_parameter_defaults.get(param), units=parameter_units.get(param)) self._param_dict.set_default(WorkhorseParameter.CLOCK_SYNCH_INTERVAL) self._param_dict.set_default(WorkhorseParameter.GET_STATUS_INTERVAL) # now we're going to build a whole 'nother param dict for the slave parameters # that contain regex values so we can fill them in easily... for param in parameter_regexes: # Scheduled events are handled by the master if WorkhorseEngineeringParameter.has(param): continue self._param_dict2.add(param + '_5th', parameter_regexes.get(param), parameter_extractors.get(param), parameter_formatters.get(param)) # ####################################################################### # Private helpers. # ####################################################################### def _got_chunk(self, chunk, timestamp, connection=None): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ if connection == SlaveProtocol.FIFTHBEAM: if self._extract_sample(VadcpCompassCalibrationDataParticle, ADCP_COMPASS_CALIBRATION_REGEX_MATCHER, chunk, timestamp): # if self.get_current_state() == WorkhorseProtocolState.COMMAND: # self._async_raise_fsm_event(WorkhorseProtocolEvent.RECOVER_AUTOSAMPLE) return if self._extract_sample(VadcpPd0SlaveDataParticle, ADCP_PD0_PARSED_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(VadcpSystemConfigurationDataParticle5, ADCP_SYSTEM_CONFIGURATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(VadcpAncillarySystemDataParticle, ADCP_ANCILLARY_SYSTEM_DATA_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(VadcpTransmitPathParticle, ADCP_TRANSMIT_PATH_REGEX_MATCHER, chunk, timestamp): return elif connection == SlaveProtocol.FOURBEAM: if self._extract_sample(AdcpCompassCalibrationDataParticle, ADCP_COMPASS_CALIBRATION_REGEX_MATCHER, chunk, timestamp): # if self.get_current_state() == WorkhorseProtocolState.COMMAND: # self._async_raise_fsm_event(WorkhorseProtocolEvent.RECOVER_AUTOSAMPLE) return if self._extract_sample(VadcpPd0BeamParsedDataParticle, ADCP_PD0_PARSED_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(VadcpSystemConfigurationDataParticle, ADCP_SYSTEM_CONFIGURATION_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(AdcpAncillarySystemDataParticle, ADCP_ANCILLARY_SYSTEM_DATA_REGEX_MATCHER, chunk, timestamp): return if self._extract_sample(AdcpTransmitPathParticle, ADCP_TRANSMIT_PATH_REGEX_MATCHER, chunk, timestamp): return def _send_break_cmd(self, delay, connection=None): """ Send a BREAK to attempt to wake the device. """ self.connections[connection].send_break(delay) def _sync_clock(self, command, date_time_param, timeout=TIMEOUT, delay=1, time_format="%Y/%m/%d,%H:%M:%S"): """ Send the command to the instrument to synchronize the clock @param command set command @param date_time_param: date time parameter that we want to set @param timeout: command timeout @param delay: wakeup delay @param time_format: time format string for set command """ log.info("SYNCING TIME WITH SENSOR.") for connection in self.connections: self._do_cmd_resp(command, date_time_param, get_timestamp_delayed("%Y/%m/%d, %H:%M:%S"), timeout=timeout, connection=connection) # ####################################################################### # Startup parameter handlers ######################################################################## def _get_params(self, parameters, connection): command = NEWLINE.join(['%s?' % p for p in parameters]) + NEWLINE if len(parameters) > 1: regex = re.compile(r'(%s.*?%s.*?>)' % (parameters[0], parameters[-1]), re.DOTALL) else: regex = re.compile(r'(%s.*?>)' % parameters[0], re.DOTALL) self._linebuf[connection] = '' self._promptbuf[connection] = '' self._do_cmd_direct(command, connection=connection) return self._get_response(response_regex=regex, connection=connection) def _update_params(self, *args, **kwargs): """ Update the parameter dictionary. """ # see if we passed in a list of parameters to query # if not, use the whole parameter list parameters = kwargs.get('params') if parameters is None or WorkhorseParameter.ALL in parameters: parameters = self._param_dict.get_keys() # filter out the engineering parameters and ALL parameters = [p for p in parameters if not WorkhorseEngineeringParameter.has(p) and p != WorkhorseParameter.ALL] # Get old param dict config. old_config = self._param_dict.get_config() if parameters: # MASTER master_params = [p for p in parameters if '_5th' not in p] if master_params: resp = self._get_params(master_params, SlaveProtocol.FOURBEAM) self._param_dict.update_many(resp) # SLAVE slave_params = [p.replace('_5th', '') for p in parameters if '_5th' in p] if slave_params: resp = self._get_params(slave_params, SlaveProtocol.FIFTHBEAM) self._param_dict2.update_many(resp) for key, value in self._param_dict2.get_all().iteritems(): self._param_dict.set_value(key, value) new_config = self._param_dict.get_config() # Check if there is any changes. Ignore TT if not dict_equal(new_config, old_config, ['TT']) or kwargs.get('force'): self._driver_event(DriverAsyncEvent.CONFIG_CHANGE) def _execute_set_params(self, commands, connection): if commands: # we are going to send the concatenation of all our set commands self._linebuf[connection] = '' self._do_cmd_direct(''.join(commands), connection=connection) # we'll need to build a regular expression to retrieve all of the responses # including any possible errors if len(commands) == 1: regex = re.compile(r'(%s.*?)\r\n>' % commands[-1].strip(), re.DOTALL) else: regex = re.compile(r'(%s.*?%s.*?)\r\n>' % (commands[0].strip(), commands[-1].strip()), re.DOTALL) response = self._get_response(response_regex=regex, connection=connection) self._parse_set_response(response[0], None) def _set_params(self, *args, **kwargs): """ Issue commands to the instrument to set various parameters """ self._verify_not_readonly(*args, **kwargs) params = args[0] changed = [] old_config = self._param_dict.get_config() master_commands = [] slave_commands = [] for key, val in params.iteritems(): if WorkhorseEngineeringParameter.has(key): continue if val != old_config.get(key): changed.append(key) if '_5th' in key: slave_commands.append(self._build_set_command( WorkhorseInstrumentCmds.SET, key.replace('_5th', ''), val)) else: master_commands.append(self._build_set_command(WorkhorseInstrumentCmds.SET, key, val)) self._execute_set_params(master_commands, connection=SlaveProtocol.FOURBEAM) self._execute_set_params(slave_commands, connection=SlaveProtocol.FIFTHBEAM) # Handle engineering parameters force = False if WorkhorseParameter.CLOCK_SYNCH_INTERVAL in params: if (params[WorkhorseParameter.CLOCK_SYNCH_INTERVAL] != self._param_dict.get( WorkhorseParameter.CLOCK_SYNCH_INTERVAL)): self._param_dict.set_value(WorkhorseParameter.CLOCK_SYNCH_INTERVAL, params[WorkhorseParameter.CLOCK_SYNCH_INTERVAL]) self.start_scheduled_job(WorkhorseParameter.CLOCK_SYNCH_INTERVAL, WorkhorseScheduledJob.CLOCK_SYNC, WorkhorseProtocolEvent.SCHEDULED_CLOCK_SYNC) force = True if WorkhorseParameter.GET_STATUS_INTERVAL in params: if (params[WorkhorseParameter.GET_STATUS_INTERVAL] != self._param_dict.get( WorkhorseParameter.GET_STATUS_INTERVAL)): self._param_dict.set_value(WorkhorseParameter.GET_STATUS_INTERVAL, params[WorkhorseParameter.GET_STATUS_INTERVAL]) self.start_scheduled_job(WorkhorseParameter.GET_STATUS_INTERVAL, WorkhorseScheduledJob.GET_CONFIGURATION, WorkhorseProtocolEvent.SCHEDULED_GET_STATUS) force = True self._update_params(params=changed, force=force) return None def _send_break(self, duration=1000, connection=None): """ Send a BREAK to attempt to wake the device. """ self._linebuf[connection] = '' self._promptbuf[connection] = '' self._send_break_cmd(duration, connection=connection) self._get_response(expected_prompt=WorkhorsePrompt.BREAK, connection=connection) def _start_logging(self, timeout=TIMEOUT, connection=None): """ Command the instrument to start logging @param timeout: how long to wait for a prompt @throws: InstrumentProtocolException if failed to start logging """ try: start = WorkhorseInstrumentCmds.START_LOGGING # start the slave first, it collects on signal from master self._do_cmd_resp(start, timeout=timeout, connection=SlaveProtocol.FIFTHBEAM) self._do_cmd_resp(start, timeout=timeout, connection=SlaveProtocol.FOURBEAM) except InstrumentException: self._stop_logging() raise def _stop_logging(self): # stop the master first (slave only collects on signal from master) self._send_break(connection=SlaveProtocol.FOURBEAM) self._send_break(connection=SlaveProtocol.FIFTHBEAM) def _discover(self, connection=None): """ Discover current state; can be COMMAND or AUTOSAMPLE or UNKNOWN. @return (next_protocol_state, next_agent_state) @throws InstrumentTimeoutException if the device cannot be woken. @throws InstrumentStateException if the device response does not correspond to an expected state. """ states = set() command = (WorkhorseProtocolState.COMMAND, ResourceAgentState.COMMAND) auto = (WorkhorseProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING) for connection in self.connections: try: self._wakeup(3, connection=connection) states.add(command) except InstrumentException: states.add(auto) if len(states) == 1: # states match, return this state return states.pop() # states don't match self._stop_logging() return command def _run_test(self, *args, **kwargs): kwargs['timeout'] = 30 kwargs['expected_prompt'] = WorkhorsePrompt.COMMAND result = [] for connection in self.connections: result.append(connection) kwargs['connection'] = connection result.append(self._do_cmd_resp(WorkhorseInstrumentCmds.RUN_TEST_200, *args, **kwargs)) return NEWLINE.join(result) @contextmanager def _pause_logging(self): try: self._stop_logging() yield finally: self._start_logging() ######################################################################## # COMMAND handlers. ######################################################################## def _handler_command_acquire_status(self, *args, **kwargs): """ execute a get status @return next_state, (next_agent_state, result) if successful. @throws InstrumentProtocolException from _do_cmd_resp. """ a = super(Protocol, self)._handler_command_acquire_status(connection=SlaveProtocol.FOURBEAM) b = super(Protocol, self)._handler_command_acquire_status(connection=SlaveProtocol.FIFTHBEAM) return None, (None, a[1][1] + b[1][1]) def _handler_command_recover_autosample(self): log.info('PD0 sample detected in COMMAND, not allowed in VADCP. Sending break') self._stop_logging() ###################################################### # DIRECT_ACCESS handlers ###################################################### def _handler_direct_access_execute_direct(self, data): next_state = None result = None next_agent_state = None self._do_cmd_direct(data) # add sent command to list for 'echo' filtering in callback self._sent_cmds.append(data) return next_state, (next_agent_state, result)
def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add( Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Flush Volume", description= "Amount of sea water to flush prior to taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Flush Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Flush Minimum Flow", description= "If the flow rate falls below this value the flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Fill Volume", description= "Amount of seawater to run through the collection filter (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Fill Flow Rate", description="Flow rate during sampling: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Fill Minimum Flow", description= "If the flow rate falls below this value the fill will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units=Prefixes.MILLI + Units.LITER, startup_param=True, display_name="Clear Volume", description= "Amount of sea water to flush the home port after taking sample: (10 - 20000)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Clear Flow Rate", description="Rate at which to flush: (100 - 250)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add( Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units=Prefixes.MILLI + Units.LITER + '/' + Units.MINUTE, startup_param=True, display_name="Clear Minimum Flow", description= "If the flow rate falls below this value the reverse flush will be terminated, " "obstruction suspected: (75 - 100)", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)
class Protocol(McLaneProtocol): """ Instrument protocol class Subclasses CommandResponseInstrumentProtocol """ def __init__(self, prompts, newline, driver_event): """ Protocol constructor. @param prompts A BaseEnum class containing instrument prompts. @param newline The newline. @param driver_event Driver process event callback. """ # Construct protocol superclass. McLaneProtocol.__init__(self, prompts, newline, driver_event) # TODO - reset next_port on mechanical refresh of the RAS bags - how is the driver notified? # TODO - need to persist state for next_port to save driver restart self.next_port = 1 # next available port # TODO - on init, perform the following commands: # - VERSION # - CAPACITY (pump flow) # - BATTERY # - CODES (termination codes) # - COPYRIGHT (termination codes) def _got_chunk(self, chunk, timestamp): """ The base class got_data has gotten a chunk from the chunker. Pass it to extract_sample with the appropriate particle objects and REGEXes. """ filling = self.get_current_state() == ProtocolState.FILL log.debug("_got_chunk:\n%s", chunk) sample_dict = self._extract_sample( RASFLSampleDataParticle, RASFLSampleDataParticle.regex_compiled(), chunk, timestamp, publish=filling) if sample_dict: self._linebuf = '' self._promptbuf = '' self._protocol_fsm.on_event( ProtocolEvent.PUMP_STATUS, RASFLSampleDataParticle.regex_compiled().search(chunk)) def _build_param_dict(self): """ Populate the parameter dictionary with XR-420 parameters. For each parameter key add value formatting function for set commands. """ # The parameter dictionary. self._param_dict = ProtocolParameterDict() # Add parameter handlers to parameter dictionary for instrument configuration parameters. self._param_dict.add(Parameter.FLUSH_VOLUME, r'Flush Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_VOLUME, units='mL', startup_param=True, display_name="flush_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_FLOWRATE, r'Flush Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_RATE, units='mL/min', startup_param=True, display_name="flush_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FLUSH_MINFLOW, r'Flush Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FLUSH_MIN_RATE, units='mL/min', startup_param=True, display_name="flush_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_VOLUME, r'Fill Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_VOLUME, units='mL', startup_param=True, display_name="fill_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_FLOWRATE, r'Fill Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_RATE, units='mL/min', startup_param=True, display_name="fill_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.FILL_MINFLOW, r'Fill Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=FILL_MIN_RATE, units='mL/min', startup_param=True, display_name="fill_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_VOLUME, r'Reverse Volume: (.*)mL', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_VOLUME, units='mL', startup_param=True, display_name="clear_volume", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_FLOWRATE, r'Reverse Flow Rate: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_RATE, units='mL/min', startup_param=True, display_name="clear_flow_rate", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.add(Parameter.CLEAR_MINFLOW, r'Reverse Min Flow: (.*)mL/min', None, self._int_to_string, type=ParameterDictType.INT, default_value=CLEAR_MIN_RATE, units='mL/min', startup_param=True, display_name="clear_min_flow", visibility=ParameterDictVisibility.IMMUTABLE) self._param_dict.set_value(Parameter.FLUSH_VOLUME, FLUSH_VOLUME) self._param_dict.set_value(Parameter.FLUSH_FLOWRATE, FLUSH_RATE) self._param_dict.set_value(Parameter.FLUSH_MINFLOW, FLUSH_MIN_RATE) self._param_dict.set_value(Parameter.FILL_VOLUME, FILL_VOLUME) self._param_dict.set_value(Parameter.FILL_FLOWRATE, FILL_RATE) self._param_dict.set_value(Parameter.FILL_MINFLOW, FILL_MIN_RATE) self._param_dict.set_value(Parameter.CLEAR_VOLUME, CLEAR_VOLUME) self._param_dict.set_value(Parameter.CLEAR_FLOWRATE, CLEAR_RATE) self._param_dict.set_value(Parameter.CLEAR_MINFLOW, CLEAR_MIN_RATE)