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
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 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(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)
class SingleConnectionInstrumentDriver(InstrumentDriver): """ Base class for instrument drivers with a single device connection. Provides connenction state logic for single connection drivers. This is the base class for the majority of driver implementation classes. """ def __init__(self, event_callback): """ Constructor for singly connected instrument drivers. @param event_callback Callback to the driver process to send asynchronous driver events back to the agent. """ InstrumentDriver.__init__(self, event_callback) # The only and only instrument connection. # Exists in the connected state. self._connection = None # The one and only instrument protocol. self._protocol = None # Build connection state machine. self._connection_fsm = ThreadSafeFSM(DriverConnectionState, DriverEvent, DriverEvent.ENTER, DriverEvent.EXIT) # Add handlers for all events. self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.ENTER, self._handler_unconfigured_enter) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.EXIT, self._handler_unconfigured_exit) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.INITIALIZE, self._handler_unconfigured_initialize) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.CONFIGURE, self._handler_unconfigured_configure) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.ENTER, self._handler_disconnected_enter) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.EXIT, self._handler_disconnected_exit) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.INITIALIZE, self._handler_disconnected_initialize) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.CONFIGURE, self._handler_disconnected_configure) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.CONNECT, self._handler_disconnected_connect) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.ENTER, self._handler_connected_enter) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.EXIT, self._handler_connected_exit) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.DISCONNECT, self._handler_connected_disconnect) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.DISCOVER, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.GET, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.SET, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.EXECUTE, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.FORCE_STATE, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.START_DIRECT, self._handler_connected_start_direct_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.STOP_DIRECT, self._handler_connected_stop_direct_event) # Start state machine. self._connection_fsm.start(DriverConnectionState.UNCONFIGURED) self._pre_da_config = {} self._startup_config = {} # Idempotency flag for lost connections. # This set to false when a connection is established to # allow for lost callback to become activated. self._connection_lost = True ############################################################# # Device connection interface. ############################################################# def initialize(self, *args, **kwargs): """ Initialize driver connection, bringing communications parameters into unconfigured state (no connection object). @raises InstrumentStateException if command not allowed in current state """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.INITIALIZE, *args, **kwargs) def configure(self, *args, **kwargs): """ Configure the driver for communications with the device via port agent / logger (valid but unconnected connection object). @param arg[0] comms config dict. @raises InstrumentStateException if command not allowed in current state @throws InstrumentParameterException if missing comms or invalid config dict. """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.CONFIGURE, *args, **kwargs) def connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger (connected connection object). @raises InstrumentStateException if command not allowed in current state @throws InstrumentConnectionException if the connection failed. """ # Forward event and argument to the connection FSM. result = self._connection_fsm.on_event(DriverEvent.CONNECT, *args, **kwargs) init_config = {} if len(args) > 0 and isinstance(args[0], dict): init_config = args[0] self.set_init_params(init_config) return result def disconnect(self, *args, **kwargs): """ Disconnect from device via port agent / logger. @raises InstrumentStateException if command not allowed in current state """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.DISCONNECT, *args, **kwargs) ############################################################# # Configuration logic ############################################################# def get_init_params(self): """ get the driver initialization parameters @return driver configuration dictionary """ return self._startup_config def set_init_params(self, config): """ Set the initialization parameters down in the protocol and store the driver configuration in the driver. If the protocol hasn't been setup yet cache the config. Next time this method is called, if you call it with an empty config it will read from the cache. @param config This default configuration assumes a structure driver configuration dict with keys named in DriverConfigKey. Stranger parameters can be adjusted by over riding this method. @raise InstrumentParameterException If the config cannot be applied """ if not isinstance(config, dict): raise InstrumentParameterException("Incompatible initialization parameters") if(self._protocol): param_config = None if(len(config)): param_config = config elif(len(self._startup_config)): param_config = self._startup_config if(param_config): self._protocol.set_init_params(param_config) self._protocol.initialize_scheduler() self._startup_config = config 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 implementation simply pushes the logic down into the protocol for processing should the action be better accomplished down there. The driver writer can decide to overload this method in the derived driver class and apply startup parameters in the driver (likely calling some get and set methods for the resource). If the driver does not implement an apply_startup_params() method in the driver, this method will call into the protocol. Deriving protocol classes are expected to implement an apply_startup_params() method lest they get the exception from the base InstrumentProtocol implementation. """ log.debug("Base driver applying startup params...") self._protocol.apply_startup_params() def get_cached_config(self): """ Return the configuration object that shows the instrument's configuration as cached in the protocol parameter dictionary. @retval The running configuration in the instruments config format. By default, it is a dictionary of parameter names and values. """ if self._protocol: return self._protocol.get_cached_config() def restore_direct_access_params(self, config): """ Restore the correct values out of the full config that is given when returning from direct access. By default, this takes a simple dict of param name and value. Override this class as needed as it makes some simple assumptions about how your instrument sets things. @param config The configuration that was previously saved (presumably to disk somewhere by the driver that is working with this protocol) """ vals = {} # for each parameter that is read only, restore da_params = self._protocol.get_direct_access_params() for param in da_params: vals[param] = config[param] log.debug("Restore DA Parameters: %s" % vals) self.set_resource(vals, True) ############################################################# # Commande and control interface. ############################################################# def discover_state(self, *args, **kwargs): """ Determine initial state upon establishing communications. @param timeout=timeout Optional command timeout. @retval Current device state. @raises InstrumentTimeoutException if could not wake device. @raises InstrumentStateException if command not allowed in current state or if device state not recognized. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.DISCOVER, DriverEvent.DISCOVER, *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. """ if self._protocol: return self._protocol.get_resource_capabilities(current_state) else: return [[], []] def get_resource_state(self, *args, **kwargs): """ Return the current state of the driver. @retval str current driver state. @raises NotImplementedException if not implemented by subclass. """ connection_state = self._connection_fsm.get_current_state() if connection_state == DriverConnectionState.CONNECTED: return self._protocol.get_current_state() else: return connection_state def get_resource(self, *args, **kwargs): """ Retrieve device parameters. @param args[0] DriverParameter.ALL or a list of parameters to retrive. @retval parameter : value dict. @raises InstrumentParameterException if missing or invalid get parameters. @raises InstrumentStateException if command not allowed in current state @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.GET, DriverEvent.GET, *args, **kwargs) def set_resource(self, *args, **kwargs): """ Set device parameters. @param args[0] parameter : value dict of parameters to set. @param timeout=timeout Optional command timeout. @raises InstrumentParameterException if missing or invalid set parameters. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if set command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.SET, DriverEvent.SET, *args, **kwargs) def execute_resource(self, resource_cmd, *args, **kwargs): """ Poll for a sample. @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.EXECUTE, resource_cmd, *args, **kwargs) def start_direct(self, *args, **kwargs): """ start direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Need to pass the event as a parameter because the event handler to capture the current # pre-da config requires it. return self._connection_fsm.on_event(DriverEvent.START_DIRECT, DriverEvent.START_DIRECT) def execute_direct(self, *args, **kwargs): """ execute direct accesscommand @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self.execute_resource(DriverEvent.EXECUTE_DIRECT, *args, **kwargs) def stop_direct(self, *args, **kwargs): """ stop direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self._connection_fsm.on_event(DriverEvent.STOP_DIRECT, DriverEvent.STOP_DIRECT) def test_force_state(self, *args, **kwargs): """ Force driver into a given state for the purposes of unit testing @param state=desired_state Required desired state to change to. @raises InstrumentParameterException if no state parameter. @raises TestModeException if not in test mode """ if(not self._test_mode): raise TestModeException(); # Get the required param state = kwargs.get('state', None) # via kwargs if state is None: raise InstrumentParameterException('Missing state parameter.') # We are mucking with internal FSM parameters which may be bad. # The alternative was to raise an event to change the state. Dont # know which is better. self._protocol._protocol_fsm.current_state = state ######################################################################## # Unconfigured handlers. ######################################################################## def _handler_unconfigured_enter(self, *args, **kwargs): """ Enter unconfigured state. """ # Send state change event to agent. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unconfigured_exit(self, *args, **kwargs): """ Exit unconfigured state. """ pass def _handler_unconfigured_initialize(self, *args, **kwargs): """ Initialize handler. We are already in unconfigured state, do nothing. @retval (next_state, result) tuple, (None, None). """ next_state = None result = None return (next_state, result) def _handler_unconfigured_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communiations config dictionary. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful, (None, None) otherwise. @raises InstrumentParameterException if missing or invalid param dict. """ next_state = None result = None # Get the required param dict. config = kwargs.get('config', None) # via kwargs # TODO use kwargs as the only mechanism if config is None: try: config = args[0] # via first argument except IndexError: pass if config is None: raise InstrumentParameterException('Missing comms config parameter.') # Verify dict and construct connection client. self._connection = self._build_connection(config) next_state = DriverConnectionState.DISCONNECTED return (next_state, result) ######################################################################## # Disconnected handlers. ######################################################################## def _handler_disconnected_enter(self, *args, **kwargs): """ Enter disconnected state. """ # Send state change event to agent. self._connection_lost = True self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_disconnected_exit(self, *args, **kwargs): """ Exit disconnected state. """ pass def _handler_disconnected_initialize(self, *args, **kwargs): """ Initialize device communications. Causes the connection parameters to be reset. @retval (next_state, result) tuple, (DriverConnectionState.UNCONFIGURED, None). """ next_state = None result = None self._connection = None next_state = DriverConnectionState.UNCONFIGURED return (next_state, result) def _handler_disconnected_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communiations config dictionary. @retval (next_state, result) tuple, (None, None). @raises InstrumentParameterException if missing or invalid param dict. """ next_state = None result = None # Get required config param dict. config = kwargs.get('config', None) # via kwargs # TODO use kwargs as the only mechanism if config is None: try: config = args[0] # via first argument except IndexError: pass if config is None: raise InstrumentParameterException('Missing comms config parameter.') # Verify configuration dict, and update connection if possible. self._connection = self._build_connection(config) return (next_state, result) def _handler_disconnected_connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger and construct and intialize a protocol FSM for device interaction. @retval (next_state, result) tuple, (DriverConnectionState.CONNECTED, None) if successful. @raises InstrumentConnectionException if the attempt to connect failed. """ next_state = None result = None self._build_protocol() self._connection.init_comms(self._protocol.got_data, self._protocol.got_raw, self._lost_connection_callback) self._protocol._connection = self._connection next_state = DriverConnectionState.CONNECTED return (next_state, result) ######################################################################## # Connected handlers. ######################################################################## def _handler_connected_enter(self, *args, **kwargs): """ Enter connected state. """ # Send state change event to agent. self._connection_lost = False self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_connected_exit(self, *args, **kwargs): """ Exit connected state. """ pass def _handler_connected_disconnect(self, *args, **kwargs): """ Disconnect to the device via port agent / logger and destroy the protocol FSM. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful. """ next_state = None result = None self._connection.stop_comms() self._protocol = None next_state = DriverConnectionState.DISCONNECTED return (next_state, result) def _handler_connected_connection_lost(self, *args, **kwargs): """ The device connection was lost. Stop comms, destroy protocol FSM and revert to disconnected state. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None). """ next_state = None result = None self._connection.stop_comms() self._protocol = None # Send async agent state change event. self._driver_event(DriverAsyncEvent.AGENT_EVENT, ResourceAgentEvent.LOST_CONNECTION) next_state = DriverConnectionState.DISCONNECTED return (next_state, result) def _handler_connected_protocol_event(self, event, *args, **kwargs): """ Forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return (next_state, result) def _handler_connected_start_direct_event(self, event, *args, **kwargs): """ Stash the current config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None self._pre_da_config = self.get_resource(DriverParameter.ALL) result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return (next_state, result) def _handler_connected_stop_direct_event(self, event, *args, **kwargs): """ Restore previous config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) self.restore_direct_access_params(self._pre_da_config) return (next_state, result) ######################################################################## # Helpers. ######################################################################## def _build_connection(self, config): """ Constructs and returns a Connection object according to the given configuration. The connection object is a LoggerClient instance in this base class. Subclasses can overwrite this operation as needed. The value returned by this operation is assigned to self._connection and also to self._protocol._connection upon entering in the DriverConnectionState.CONNECTED state. @param config configuration dict @retval a Connection instance, which will be assigned to self._connection @throws InstrumentParameterException Invalid configuration. """ if 'mock_port_agent' in config: mock_port_agent = config['mock_port_agent'] # check for validity here... if (mock_port_agent is not None): return mock_port_agent try: addr = config['addr'] port = config['port'] cmd_port = config.get('cmd_port') if isinstance(addr, str) and isinstance(port, int) and len(addr)>0: return PortAgentClient(addr, port, cmd_port) else: raise InstrumentParameterException('Invalid comms config dict.') except (TypeError, KeyError): raise InstrumentParameterException('Invalid comms config dict.') def _lost_connection_callback(self, error_string): """ A callback invoked by the port agent client when it looses connectivity to the port agent. """ if not self._connection_lost: self._connection_lost = True lost_comms_thread = Thread( target=self._connection_fsm.on_event, args=(DriverEvent.CONNECTION_LOST, )) lost_comms_thread.start() def _build_protocol(self): """ Construct device specific single connection protocol FSM. Overridden in device specific subclasses. """ pass
class SingleConnectionInstrumentDriver(InstrumentDriver): """ Base class for instrument drivers with a single device connection. Provides connection state logic for single connection drivers. This is the base class for the majority of driver implementation classes. """ __metaclass__ = META_LOGGER def __init__(self, event_callback, refdes=None): """ Constructor for singly connected instrument drivers. @param event_callback Callback to the driver process to send asynchronous driver events back to the agent. """ InstrumentDriver.__init__(self, event_callback) # The one and only instrument connection. # Exists in the connected state. self._connection = None # The one and only instrument protocol. self._protocol = None # Consul self.consul = consulate.Consul() # Reference Designator to the port agent service self.refdes = refdes # Build connection state machine. self._connection_fsm = ThreadSafeFSM(DriverConnectionState, DriverEvent, DriverEvent.ENTER, DriverEvent.EXIT) # Add handlers for all events. handlers = { DriverState.UNCONFIGURED: [ (DriverEvent.ENTER, self._handler_unconfigured_enter), (DriverEvent.EXIT, self._handler_unconfigured_exit), (DriverEvent.INITIALIZE, self._handler_unconfigured_initialize), (DriverEvent.CONFIGURE, self._handler_unconfigured_configure), ], DriverConnectionState.DISCONNECTED: [ (DriverEvent.ENTER, self._handler_disconnected_enter), (DriverEvent.EXIT, self._handler_disconnected_exit), (DriverEvent.INITIALIZE, self._handler_disconnected_initialize), (DriverEvent.CONFIGURE, self._handler_disconnected_configure), (DriverEvent.CONNECT, self._handler_disconnected_connect), ], DriverConnectionState.CONNECTED: [ (DriverEvent.ENTER, self._handler_connected_enter), (DriverEvent.EXIT, self._handler_connected_exit), (DriverEvent.DISCONNECT, self._handler_connected_disconnect), (DriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost), (DriverEvent.DISCOVER, self._handler_connected_protocol_event), (DriverEvent.GET, self._handler_connected_protocol_event), (DriverEvent.SET, self._handler_connected_protocol_event), (DriverEvent.EXECUTE, self._handler_connected_protocol_event), (DriverEvent.FORCE_STATE, self._handler_connected_protocol_event), (DriverEvent.START_DIRECT, self._handler_connected_start_direct_event), (DriverEvent.STOP_DIRECT, self._handler_connected_stop_direct_event), ], } for state in handlers: for event, handler in handlers[state]: self._connection_fsm.add_handler(state, event, handler) self._pre_da_config = {} self._startup_config = {} # Idempotency flag for lost connections. # This set to false when a connection is established to # allow for lost callback to become activated. self._connection_lost = True # Autoconnect flag # Set this to false to disable autoconnect self._autoconnect = True self._reconnect_interval = STARTING_RECONNECT_INTERVAL self._max_reconnect_interval = MAXIMUM_RECONNECT_INTERVAL # Start state machine. self._connection_fsm.start(DriverConnectionState.UNCONFIGURED) ############################################################# # Device connection interface. ############################################################# def initialize(self, *args, **kwargs): """ Initialize driver connection, bringing communications parameters into unconfigured state (no connection object). @raises InstrumentStateException if command not allowed in current state """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.INITIALIZE, *args, **kwargs) def configure(self, *args, **kwargs): """ Configure the driver for communications with the device via port agent / logger (valid but unconnected connection object). @param arg[0] comms config dict. @raises InstrumentStateException if command not allowed in current state @throws InstrumentParameterException if missing comms or invalid config dict. """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.CONFIGURE, *args, **kwargs) def connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger (connected connection object). @raises InstrumentStateException if command not allowed in current state @throws InstrumentConnectionException if the connection failed. """ # Forward event and argument to the connection FSM. result = self._connection_fsm.on_event(DriverEvent.CONNECT, *args, **kwargs) init_config = {} if len(args) > 0 and isinstance(args[0], dict): init_config = args[0] self.set_init_params(init_config) return result def disconnect(self, *args, **kwargs): """ Disconnect from device via port agent / logger. @raises InstrumentStateException if command not allowed in current state """ # Disable autoconnect if manually disconnected self._autoconnect = False # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.DISCONNECT, *args, **kwargs) ############################################################# # Configuration logic ############################################################# def get_init_params(self): """ get the driver initialization parameters @return driver configuration dictionary """ return self._startup_config def set_init_params(self, config): """ Set the initialization parameters down in the protocol and store the driver configuration in the driver. If the protocol hasn't been setup yet cache the config. Next time this method is called, if you call it with an empty config it will read from the cache. @param config This default configuration assumes a structure driver configuration dict with keys named in DriverConfigKey. Stranger parameters can be adjusted by over riding this method. @raise InstrumentParameterException If the config cannot be applied """ if not isinstance(config, dict): raise InstrumentParameterException("Incompatible initialization parameters") if self._protocol: param_config = None if config: param_config = config elif self._startup_config: param_config = self._startup_config if param_config: self._protocol.set_init_params(param_config) self._protocol.initialize_scheduler() if config: self._startup_config = config 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 implementation simply pushes the logic down into the protocol for processing should the action be better accomplished down there. The driver writer can decide to overload this method in the derived driver class and apply startup parameters in the driver (likely calling some get and set methods for the resource). If the driver does not implement an apply_startup_params() method in the driver, this method will call into the protocol. Deriving protocol classes are expected to implement an apply_startup_params() method lest they get the exception from the base InstrumentProtocol implementation. """ log.debug("Base driver applying startup params...") self._protocol.apply_startup_params() def get_cached_config(self): """ Return the configuration object that shows the instrument's configuration as cached in the protocol parameter dictionary. @retval The running configuration in the instruments config format. By default, it is a dictionary of parameter names and values. """ if self._protocol: return self._protocol.get_cached_config() 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...") protocol = self._protocol # Because the config requires information from the protocol param dict # we temporarily instantiate a protocol object to get at the static # information. if not protocol: self._build_protocol() log.debug("Getting metadata from protocol...") return self._protocol.get_config_metadata_dict() def restore_direct_access_params(self, config): """ Restore the correct values out of the full config that is given when returning from direct access. By default, this takes a simple dict of param name and value. Override this class as needed as it makes some simple assumptions about how your instrument sets things. @param config The configuration that was previously saved (presumably to disk somewhere by the driver that is working with this protocol) """ vals = {} # for each parameter that is read only, restore da_params = self._protocol.get_direct_access_params() for param in da_params: vals[param] = config[param] log.debug("Restore DA Parameters: %r", vals) self.set_resource(vals, True) ############################################################# # Command and control interface. ############################################################# def discover_state(self, *args, **kwargs): """ Determine initial state upon establishing communications. @param timeout=timeout Optional command timeout. @retval Current device state. @raises InstrumentTimeoutException if could not wake device. @raises InstrumentStateException if command not allowed in current state or if device state not recognized. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event( DriverEvent.DISCOVER, DriverEvent.DISCOVER, *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 return all commands. @retval list of AgentCapability objects representing the drivers capabilities. @raises NotImplementedException if not implemented by subclass. """ if self._protocol: return self._protocol.get_resource_capabilities(current_state) else: return [[], []] def get_resource_state(self, *args, **kwargs): """ Return the current state of the driver. @retval str current driver state. @raises NotImplementedException if not implemented by subclass. """ connection_state = self._connection_fsm.get_current_state() if connection_state == DriverConnectionState.CONNECTED: return self._protocol.get_current_state() else: return connection_state def get_resource(self, *args, **kwargs): """ Retrieve device parameters. @param args[0] DriverParameter.ALL or a list of parameters to retrieve. @retval parameter : value dict. @raises InstrumentParameterException if missing or invalid get parameters. @raises InstrumentStateException if command not allowed in current state @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.GET, DriverEvent.GET, *args, **kwargs) def set_resource(self, *args, **kwargs): """ Set device parameters. @param args[0] parameter : value dict of parameters to set. @param timeout=timeout Optional command timeout. @raises InstrumentParameterException if missing or invalid set parameters. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if set command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.SET, DriverEvent.SET, *args, **kwargs) def execute_resource(self, resource_cmd, *args, **kwargs): """ Poll for a sample. @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.EXECUTE, resource_cmd, *args, **kwargs) def start_direct(self, *args, **kwargs): """ start direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Need to pass the event as a parameter because the event handler to capture the current # pre-da config requires it. return self._connection_fsm.on_event(DriverEvent.START_DIRECT, DriverEvent.START_DIRECT) def execute_direct(self, *args, **kwargs): """ execute direct access command @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self.execute_resource(DriverEvent.EXECUTE_DIRECT, *args, **kwargs) def stop_direct(self, *args, **kwargs): """ stop direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self._connection_fsm.on_event(DriverEvent.STOP_DIRECT, DriverEvent.STOP_DIRECT) def test_force_state(self, *args, **kwargs): """ Force driver into a given state for the purposes of unit testing @param state=desired_state Required desired state to change to. @raises InstrumentParameterException if no state parameter. @raises TestModeException if not in test mode """ if not self._test_mode: raise TestModeException() # Get the required param state = kwargs.get('state', None) # via kwargs if state is None: raise InstrumentParameterException('Missing state parameter.') # We are mucking with internal FSM parameters which may be bad. # The alternative was to raise an event to change the state. Don't # know which is better. self._protocol._protocol_fsm.current_state = state ######################################################################## # Unconfigured handlers. ######################################################################## def _handler_unconfigured_enter(self, *args, **kwargs): """ Enter unconfigured state. """ # Send state change event to agent. self._driver_event(DriverAsyncEvent.STATE_CHANGE) # attempt to auto-configure from consul self._auto_config_with_backoff() def _handler_unconfigured_exit(self, *args, **kwargs): """ Exit unconfigured state. """ pass def _handler_unconfigured_initialize(self, *args, **kwargs): """ Initialize handler. We are already in unconfigured state, do nothing. @retval (next_state, result) tuple, (None, None). """ return None, None def _handler_unconfigured_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communications config dictionary. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful, (None, None) otherwise. @raises InstrumentParameterException if missing or invalid param dict. """ # Verify configuration dict, and update connection if possible. try: self._connection = self._build_connection(*args, **kwargs) except InstrumentException: self._auto_config_with_backoff() raise return DriverConnectionState.DISCONNECTED, None ######################################################################## # Disconnected handlers. ######################################################################## def _handler_disconnected_enter(self, *args, **kwargs): """ Enter disconnected state. """ # Send state change event to agent. self._driver_event(DriverAsyncEvent.STATE_CHANGE) if self._autoconnect: self._async_raise_event(DriverEvent.CONNECT, *args, **kwargs) def _handler_disconnected_exit(self, *args, **kwargs): """ Exit disconnected state. """ pass def _handler_disconnected_initialize(self, *args, **kwargs): """ Initialize device communications. Causes the connection parameters to be reset. @retval (next_state, result) tuple, (DriverConnectionState.UNCONFIGURED, None). """ self._connection = None return DriverConnectionState.UNCONFIGURED, None def _handler_disconnected_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communications config dictionary. @retval (next_state, result) tuple, (None, None). @raises InstrumentParameterException if missing or invalid param dict. """ # Verify configuration dict, and update connection if possible. self._connection = self._build_connection(*args, **kwargs) return DriverConnectionState.UNCONFIGURED, None def _handler_disconnected_connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger and construct and initialize a protocol FSM for device interaction. @retval (next_state, result) tuple, (DriverConnectionState.CONNECTED, None) if successful. @raises InstrumentConnectionException if the attempt to connect failed. """ result = None self._build_protocol() try: self._connection.init_comms(self._protocol.got_data, self._protocol.got_raw, self._got_config, self._got_exception, self._lost_connection_callback) self._protocol._connection = self._connection next_state = DriverConnectionState.CONNECTED except InstrumentConnectionException as e: log.error("Connection Exception: %s", e) log.error("Instrument Driver returning to unconfigured state.") next_state = DriverConnectionState.UNCONFIGURED return next_state, result ######################################################################## # Connected handlers. ######################################################################## def _handler_connected_enter(self, *args, **kwargs): """ Enter connected state. """ # Send state change event to agent. self._connection_lost = False # reset the reconnection interval to 1 self._reconnect_interval = STARTING_RECONNECT_INTERVAL self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_connected_exit(self, *args, **kwargs): """ Exit connected state. """ pass def _handler_connected_disconnect(self, *args, **kwargs): """ Disconnect to the device via port agent / logger and destroy the protocol FSM. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful. """ log.info("_handler_connected_disconnect: invoking stop_comms().") self._connection.stop_comms() scheduler = self._protocol._scheduler if scheduler: scheduler._scheduler.shutdown() scheduler = None self._protocol = None return DriverConnectionState.UNCONFIGURED, None def _handler_connected_connection_lost(self, *args, **kwargs): """ The device connection was lost. Stop comms, destroy protocol FSM and revert to disconnected state. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None). """ log.info("_handler_connected_connection_lost: invoking stop_comms().") self._connection.stop_comms() scheduler = self._protocol._scheduler if scheduler: scheduler._scheduler.shutdown() scheduler = None self._protocol = None # Send async agent state change event. log.info("_handler_connected_connection_lost: sending LOST_CONNECTION " "event, moving to UNCONFIGURED state.") self._driver_event(DriverAsyncEvent.AGENT_EVENT, ResourceAgentEvent.LOST_CONNECTION) return DriverConnectionState.UNCONFIGURED, None def _handler_connected_protocol_event(self, event, *args, **kwargs): """ Forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return next_state, result def _handler_connected_start_direct_event(self, event, *args, **kwargs): """ Stash the current config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None # Get the value for all direct access parameters and store them in the protocol self._pre_da_config = self.get_resource(self._protocol.get_direct_access_params()) self._protocol.store_direct_access_config(self._pre_da_config) self._protocol.enable_da_initialization() log.debug("starting DA. Storing DA parameters for restore: %s", self._pre_da_config) result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return next_state, result def _handler_connected_stop_direct_event(self, event, *args, **kwargs): """ Restore previous config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) # Moving the responsibility for applying DA parameters to the # protocol. # self.restore_direct_access_params(self._pre_da_config) return next_state, result ######################################################################## # Helpers. ######################################################################## def _build_connection(self, *args, **kwargs): """ Constructs and returns a Connection object according to the given configuration. The connection object is a LoggerClient instance in this base class. Subclasses can overwrite this operation as needed. The value returned by this operation is assigned to self._connection and also to self._protocol._connection upon entering in the DriverConnectionState.CONNECTED state. @param config configuration dict @retval a Connection instance, which will be assigned to self._connection @throws InstrumentParameterException Invalid configuration. """ # Get required config param dict. config = kwargs.get('config', None) # via kwargs if config is None and len(args) > 0: config = args[0] # via first argument if config is None: config = self._get_config_from_consul(self.refdes) if config is None: raise InstrumentParameterException('No port agent config supplied and failed to auto-discover port agent') if 'mock_port_agent' in config: mock_port_agent = config['mock_port_agent'] # check for validity here... if mock_port_agent is not None: return mock_port_agent try: addr = config['addr'] port = config['port'] cmd_port = config.get('cmd_port') if isinstance(addr, basestring) and isinstance(port, int) and len(addr) > 0: return PortAgentClient(addr, port, cmd_port) else: raise InstrumentParameterException('Invalid comms config dict.') except (TypeError, KeyError): raise InstrumentParameterException('Invalid comms config dict.') def _get_config_from_consul(self, tag): """ Query consul for the port agent service configuration parameters: data port, command port, and address This will retry a specified number of times with exponential backoff. """ try: data_port = self.consul.health.service('port-agent', passing=True, tag=tag) cmd_port = self.consul.health.service('command-port-agent', passing=True, tag=tag) if data_port and cmd_port: port = data_port[0]['Service']['Port'] addr = data_port[0]['Node']['Address'] cmd_port = cmd_port[0]['Service']['Port'] port_agent_config = {'port': port, 'cmd_port': cmd_port, 'addr': addr} return port_agent_config except ConnectionError: return None def _got_exception(self, exception): """ Callback for the client for exception handling with async data. Exceptions are wrapped in an event and sent up to the agent. """ try: log.error("ASYNC Data Exception Detected: %s (%s)", exception.__class__.__name__, str(exception)) finally: self._driver_event(DriverAsyncEvent.ERROR, exception) def _got_config(self, port_agent_packet): data = port_agent_packet.get_data() configuration = {} for each in data.split('\n'): if each == '': continue key, value = each.split(None, 1) try: value = int(value) except ValueError: pass configuration[key] = value self._driver_event(DriverAsyncEvent.DRIVER_CONFIG, configuration) def _lost_connection_callback(self, error_string): """ A callback invoked by the port agent client when it looses connectivity to the port agent. """ if not self._connection_lost: log.info("_lost_connection_callback: starting thread to send " "CONNECTION_LOST event to instrument driver.") self._connection_lost = True self._async_raise_event(DriverEvent.CONNECTION_LOST) else: log.info("_lost_connection_callback: connection_lost flag true.") def _build_protocol(self): """ Construct device specific single connection protocol FSM. Overridden in device specific subclasses. """ pass def _auto_config_with_backoff(self): # randomness to prevent all instrument drivers from trying to reconnect at the same exact time. self._reconnect_interval = self._reconnect_interval * 2 + random.uniform(-.5, .5) interval = min(self._reconnect_interval, self._max_reconnect_interval) self._async_raise_event(DriverEvent.CONFIGURE, event_delay=interval) log.info('Created delayed CONFIGURE event with %.2f second delay', interval) def _async_raise_event(self, event, *args, **kwargs): delay = kwargs.pop('event_delay', 0) def inner(): try: time.sleep(delay) log.info('Async raise event: %r', event) self._connection_fsm.on_event(event) except Exception as exc: log.exception('Exception in asynchronous thread: %r', exc) self._driver_event(DriverAsyncEvent.ERROR, exc) log.info('_async_raise_fsm_event: event complete. bub bye thread. (%r)', args) thread = Thread(target=inner) thread.start()
class SingleConnectionInstrumentDriver(InstrumentDriver): """ Base class for instrument drivers with a single device connection. Provides connenction state logic for single connection drivers. This is the base class for the majority of driver implementation classes. """ def __init__(self, event_callback): """ Constructor for singly connected instrument drivers. @param event_callback Callback to the driver process to send asynchronous driver events back to the agent. """ InstrumentDriver.__init__(self, event_callback) # The only and only instrument connection. # Exists in the connected state. self._connection = None # The one and only instrument protocol. self._protocol = None # Build connection state machine. self._connection_fsm = ThreadSafeFSM(DriverConnectionState, DriverEvent, DriverEvent.ENTER, DriverEvent.EXIT) # Add handlers for all events. self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.ENTER, self._handler_unconfigured_enter) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.EXIT, self._handler_unconfigured_exit) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.INITIALIZE, self._handler_unconfigured_initialize) self._connection_fsm.add_handler(DriverConnectionState.UNCONFIGURED, DriverEvent.CONFIGURE, self._handler_unconfigured_configure) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.ENTER, self._handler_disconnected_enter) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.EXIT, self._handler_disconnected_exit) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.INITIALIZE, self._handler_disconnected_initialize) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.CONFIGURE, self._handler_disconnected_configure) self._connection_fsm.add_handler(DriverConnectionState.DISCONNECTED, DriverEvent.CONNECT, self._handler_disconnected_connect) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.ENTER, self._handler_connected_enter) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.EXIT, self._handler_connected_exit) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.DISCONNECT, self._handler_connected_disconnect) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.DISCOVER, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.GET, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.SET, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.EXECUTE, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.FORCE_STATE, self._handler_connected_protocol_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.START_DIRECT, self._handler_connected_start_direct_event) self._connection_fsm.add_handler(DriverConnectionState.CONNECTED, DriverEvent.STOP_DIRECT, self._handler_connected_stop_direct_event) # Start state machine. self._connection_fsm.start(DriverConnectionState.UNCONFIGURED) self._pre_da_config = {} self._startup_config = {} # Idempotency flag for lost connections. # This set to false when a connection is established to # allow for lost callback to become activated. self._connection_lost = True ############################################################# # Device connection interface. ############################################################# def initialize(self, *args, **kwargs): """ Initialize driver connection, bringing communications parameters into unconfigured state (no connection object). @raises InstrumentStateException if command not allowed in current state """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.INITIALIZE, *args, **kwargs) def configure(self, *args, **kwargs): """ Configure the driver for communications with the device via port agent / logger (valid but unconnected connection object). @param arg[0] comms config dict. @raises InstrumentStateException if command not allowed in current state @throws InstrumentParameterException if missing comms or invalid config dict. """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.CONFIGURE, *args, **kwargs) def connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger (connected connection object). @raises InstrumentStateException if command not allowed in current state @throws InstrumentConnectionException if the connection failed. """ # Forward event and argument to the connection FSM. result = self._connection_fsm.on_event(DriverEvent.CONNECT, *args, **kwargs) init_config = {} if len(args) > 0 and isinstance(args[0], dict): init_config = args[0] self.set_init_params(init_config) return result def disconnect(self, *args, **kwargs): """ Disconnect from device via port agent / logger. @raises InstrumentStateException if command not allowed in current state """ # Forward event and argument to the connection FSM. return self._connection_fsm.on_event(DriverEvent.DISCONNECT, *args, **kwargs) ############################################################# # Configuration logic ############################################################# def get_init_params(self): """ get the driver initialization parameters @return driver configuration dictionary """ return self._startup_config def set_init_params(self, config): """ Set the initialization parameters down in the protocol and store the driver configuration in the driver. If the protocol hasn't been setup yet cache the config. Next time this method is called, if you call it with an empty config it will read from the cache. @param config This default configuration assumes a structure driver configuration dict with keys named in DriverConfigKey. Stranger parameters can be adjusted by over riding this method. @raise InstrumentParameterException If the config cannot be applied """ if not isinstance(config, dict): raise InstrumentParameterException("Incompatible initialization parameters") if self._protocol: param_config = None if config: param_config = config elif self._startup_config: param_config = self._startup_config if param_config: self._protocol.set_init_params(param_config) self._protocol.initialize_scheduler() if config: self._startup_config = config 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 implementation simply pushes the logic down into the protocol for processing should the action be better accomplished down there. The driver writer can decide to overload this method in the derived driver class and apply startup parameters in the driver (likely calling some get and set methods for the resource). If the driver does not implement an apply_startup_params() method in the driver, this method will call into the protocol. Deriving protocol classes are expected to implement an apply_startup_params() method lest they get the exception from the base InstrumentProtocol implementation. """ log.debug("Base driver applying startup params...") self._protocol.apply_startup_params() def get_cached_config(self): """ Return the configuration object that shows the instrument's configuration as cached in the protocol parameter dictionary. @retval The running configuration in the instruments config format. By default, it is a dictionary of parameter names and values. """ if self._protocol: return self._protocol.get_cached_config() 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...") protocol = self._protocol # Because the config requires information from the protocol param dict # we temporarily instantiate a protocol object to get at the static # information. if not protocol: self._build_protocol() log.debug("Getting metadata from protocol...") return json.dumps(self._protocol.get_config_metadata_dict(), sort_keys=True) def restore_direct_access_params(self, config): """ Restore the correct values out of the full config that is given when returning from direct access. By default, this takes a simple dict of param name and value. Override this class as needed as it makes some simple assumptions about how your instrument sets things. @param config The configuration that was previously saved (presumably to disk somewhere by the driver that is working with this protocol) """ vals = {} # for each parameter that is read only, restore da_params = self._protocol.get_direct_access_params() for param in da_params: vals[param] = config[param] log.debug("Restore DA Parameters: %s" % vals) self.set_resource(vals, True) ############################################################# # Commande and control interface. ############################################################# def discover_state(self, *args, **kwargs): """ Determine initial state upon establishing communications. @param timeout=timeout Optional command timeout. @retval Current device state. @raises InstrumentTimeoutException if could not wake device. @raises InstrumentStateException if command not allowed in current state or if device state not recognized. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.DISCOVER, DriverEvent.DISCOVER, *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. """ if self._protocol: return self._protocol.get_resource_capabilities(current_state) else: return [['foobb'], ['fooaa']] def get_resource_state(self, *args, **kwargs): """ Return the current state of the driver. @retval str current driver state. @raises NotImplementedException if not implemented by subclass. """ connection_state = self._connection_fsm.get_current_state() if connection_state == DriverConnectionState.CONNECTED: return self._protocol.get_current_state() else: return connection_state def get_resource(self, *args, **kwargs): """ Retrieve device parameters. @param args[0] DriverParameter.ALL or a list of parameters to retrive. @retval parameter : value dict. @raises InstrumentParameterException if missing or invalid get parameters. @raises InstrumentStateException if command not allowed in current state @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.GET, DriverEvent.GET, *args, **kwargs) def set_resource(self, *args, **kwargs): """ Set device parameters. @param args[0] parameter : value dict of parameters to set. @param timeout=timeout Optional command timeout. @raises InstrumentParameterException if missing or invalid set parameters. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if set command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.SET, DriverEvent.SET, *args, **kwargs) def execute_resource(self, resource_cmd, *args, **kwargs): """ Poll for a sample. @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Forward event and argument to the protocol FSM. return self._connection_fsm.on_event(DriverEvent.EXECUTE, resource_cmd, *args, **kwargs) def start_direct(self, *args, **kwargs): """ start direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ # Need to pass the event as a parameter because the event handler to capture the current # pre-da config requires it. return self._connection_fsm.on_event(DriverEvent.START_DIRECT, DriverEvent.START_DIRECT) def execute_direct(self, *args, **kwargs): """ execute direct accesscommand @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self.execute_resource(DriverEvent.EXECUTE_DIRECT, *args, **kwargs) def stop_direct(self, *args, **kwargs): """ stop direct access mode @param timeout=timeout Optional command timeout. @ retval Device sample dict. @raises InstrumentTimeoutException if could not wake device or no response. @raises InstrumentProtocolException if acquire command not recognized. @raises InstrumentStateException if command not allowed in current state. @raises NotImplementedException if not implemented by subclass. """ return self._connection_fsm.on_event(DriverEvent.STOP_DIRECT, DriverEvent.STOP_DIRECT) def test_force_state(self, *args, **kwargs): """ Force driver into a given state for the purposes of unit testing @param state=desired_state Required desired state to change to. @raises InstrumentParameterException if no state parameter. @raises TestModeException if not in test mode """ if(not self._test_mode): raise TestModeException(); # Get the required param state = kwargs.get('state', None) # via kwargs if state is None: raise InstrumentParameterException('Missing state parameter.') # We are mucking with internal FSM parameters which may be bad. # The alternative was to raise an event to change the state. Dont # know which is better. self._protocol._protocol_fsm.current_state = state ######################################################################## # Unconfigured handlers. ######################################################################## def _handler_unconfigured_enter(self, *args, **kwargs): """ Enter unconfigured state. """ # Send state change event to agent. self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_unconfigured_exit(self, *args, **kwargs): """ Exit unconfigured state. """ pass def _handler_unconfigured_initialize(self, *args, **kwargs): """ Initialize handler. We are already in unconfigured state, do nothing. @retval (next_state, result) tuple, (None, None). """ next_state = None result = None return (next_state, result) def _handler_unconfigured_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communiations config dictionary. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful, (None, None) otherwise. @raises InstrumentParameterException if missing or invalid param dict. """ next_state = None result = None # Get the required param dict. config = kwargs.get('config', None) # via kwargs # TODO use kwargs as the only mechanism if config is None: try: config = args[0] # via first argument except IndexError: pass if config is None: raise InstrumentParameterException('Missing comms config parameter.') # Verify dict and construct connection client. self._connection = self._build_connection(config) next_state = DriverConnectionState.DISCONNECTED return (next_state, result) ######################################################################## # Disconnected handlers. ######################################################################## def _handler_disconnected_enter(self, *args, **kwargs): """ Enter disconnected state. """ # Send state change event to agent. self._connection_lost = True self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_disconnected_exit(self, *args, **kwargs): """ Exit disconnected state. """ pass def _handler_disconnected_initialize(self, *args, **kwargs): """ Initialize device communications. Causes the connection parameters to be reset. @retval (next_state, result) tuple, (DriverConnectionState.UNCONFIGURED, None). """ next_state = None result = None self._connection = None next_state = DriverConnectionState.UNCONFIGURED return (next_state, result) def _handler_disconnected_configure(self, *args, **kwargs): """ Configure driver for device comms. @param args[0] Communiations config dictionary. @retval (next_state, result) tuple, (None, None). @raises InstrumentParameterException if missing or invalid param dict. """ next_state = None result = None # Get required config param dict. config = kwargs.get('config', None) # via kwargs # TODO use kwargs as the only mechanism if config is None: try: config = args[0] # via first argument except IndexError: pass if config is None: raise InstrumentParameterException('Missing comms config parameter.') # Verify configuration dict, and update connection if possible. self._connection = self._build_connection(config) return (next_state, result) def _handler_disconnected_connect(self, *args, **kwargs): """ Establish communications with the device via port agent / logger and construct and intialize a protocol FSM for device interaction. @retval (next_state, result) tuple, (DriverConnectionState.CONNECTED, None) if successful. @raises InstrumentConnectionException if the attempt to connect failed. """ next_state = None result = None self._build_protocol() try: self._connection.init_comms(self._protocol.got_data, self._protocol.got_raw, self._got_exception, self._lost_connection_callback) self._protocol._connection = self._connection next_state = DriverConnectionState.CONNECTED except InstrumentConnectionException as e: log.error("Connection Exception: %s", e) log.error("Instrument Driver remaining in disconnected state.") # Re-raise the exception raise return (next_state, result) ######################################################################## # Connected handlers. ######################################################################## def _handler_connected_enter(self, *args, **kwargs): """ Enter connected state. """ # Send state change event to agent. self._connection_lost = False self._driver_event(DriverAsyncEvent.STATE_CHANGE) def _handler_connected_exit(self, *args, **kwargs): """ Exit connected state. """ pass def _handler_connected_disconnect(self, *args, **kwargs): """ Disconnect to the device via port agent / logger and destroy the protocol FSM. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None) if successful. """ next_state = None result = None log.info("_handler_connected_disconnect: invoking stop_comms().") self._connection.stop_comms() self._protocol = None next_state = DriverConnectionState.DISCONNECTED return (next_state, result) def _handler_connected_connection_lost(self, *args, **kwargs): """ The device connection was lost. Stop comms, destroy protocol FSM and revert to disconnected state. @retval (next_state, result) tuple, (DriverConnectionState.DISCONNECTED, None). """ next_state = None result = None log.info("_handler_connected_connection_lost: invoking stop_comms().") self._connection.stop_comms() self._protocol = None # Send async agent state change event. log.info("_handler_connected_connection_lost: sending LOST_CONNECTION " \ "event, moving to DISCONNECTED state.") self._driver_event(DriverAsyncEvent.AGENT_EVENT, ResourceAgentEvent.LOST_CONNECTION) next_state = DriverConnectionState.DISCONNECTED return (next_state, result) def _handler_connected_protocol_event(self, event, *args, **kwargs): """ Forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return (next_state, result) def _handler_connected_start_direct_event(self, event, *args, **kwargs): """ Stash the current config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None # Get the value for all direct access parameters and store them in the protocol self._pre_da_config = self.get_resource(self._protocol.get_direct_access_params()) self._protocol.store_direct_access_config(self._pre_da_config) self._protocol.enable_da_initialization() log.debug("starting DA. Storing DA parameters for restore: %s", self._pre_da_config) result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) return (next_state, result) def _handler_connected_stop_direct_event(self, event, *args, **kwargs): """ Restore previous config first, then forward a driver command event to the protocol FSM. @param args positional arguments to pass on. @param kwargs keyword arguments to pass on. @retval (next_state, result) tuple, (None, protocol result). """ next_state = None result = self._protocol._protocol_fsm.on_event(event, *args, **kwargs) # Moving the responsibility for applying DA parameters to the # protocol. #self.restore_direct_access_params(self._pre_da_config) return (next_state, result) ######################################################################## # Helpers. ######################################################################## def _build_connection(self, config): """ Constructs and returns a Connection object according to the given configuration. The connection object is a LoggerClient instance in this base class. Subclasses can overwrite this operation as needed. The value returned by this operation is assigned to self._connection and also to self._protocol._connection upon entering in the DriverConnectionState.CONNECTED state. @param config configuration dict @retval a Connection instance, which will be assigned to self._connection @throws InstrumentParameterException Invalid configuration. """ if 'mock_port_agent' in config: mock_port_agent = config['mock_port_agent'] # check for validity here... if (mock_port_agent is not None): return mock_port_agent try: addr = config['addr'] port = config['port'] cmd_port = config.get('cmd_port') if isinstance(addr, str) and isinstance(port, int) and len(addr)>0: return PortAgentClient(addr, port, cmd_port) else: raise InstrumentParameterException('Invalid comms config dict.') except (TypeError, KeyError): raise InstrumentParameterException('Invalid comms config dict.') def _got_exception(self, exception): """ Callback for the client for exception handling with async data. Exceptions are wrapped in an event and sent up to the agent. """ try: log.error("ASYNC Data Exception Detected: %s (%s)", exception.__class__.__name__, str(exception)) finally: self._driver_event(DriverAsyncEvent.ERROR, exception) def _lost_connection_callback(self, error_string): """ A callback invoked by the port agent client when it looses connectivity to the port agent. """ if not self._connection_lost: log.info("_lost_connection_callback: starting thread to send " \ "CONNECTION_LOST event to instrument driver.") self._connection_lost = True lost_comms_thread = Thread( target=self._connection_fsm.on_event, args=(DriverEvent.CONNECTION_LOST, )) lost_comms_thread.start() else: log.info("_lost_connection_callback: connection_lost flag true.") def _build_protocol(self): """ Construct device specific single connection protocol FSM. Overridden in device specific subclasses. """ pass
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