Beispiel #1
0
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)
Beispiel #2
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    __metaclass__ = get_logging_metaclass(log_level='trace')

    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.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.START_AUTOSAMPLE, self._handler_command_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_STATUS, self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.GET, self._handler_command_get)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add sample handlers.

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(self.sieve_function)

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        self._param_dict.add(Parameter.SCHEDULE,
                             r'schedule:\s+(.*)',
                             lambda match: match.group(1),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="Schedule",
                             description="Large block of text used to create the .yaml file defining the sampling schedule.",
                             startup_param=True,
                             default_value=yaml.dump(DEFAULT_CONFIG, default_flow_style=False))

        self._param_dict.add(Parameter.FTP_IP_ADDRESS,
                             r'ftp address:\s+(\d\d\d\d\.\d\d\d\d\.\d\d\d\d\.\d\d\d)',
                             lambda match: match.group(1),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="FTP IP Address",
                             description="IP address the driver uses to connect to the instrument FTP server.",
                             startup_param=True,
                             default_value=DEFAULT_HOST)

        self._param_dict.add(Parameter.FTP_USERNAME,
                             r'username:(.*)',
                             lambda match: match.group(1),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="FTP User Name",
                             description="Username used to connect to the FTP server.",
                             startup_param=True,
                             default_value=USER_NAME)

        self._param_dict.add(Parameter.FTP_PASSWORD,
                             r'password:(.*)',
                             lambda match: match.group(1),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="FTP Password",
                             description="Password used to connect to the FTP server.",
                             startup_param=True,
                             default_value=PASSWORD)

        self._param_dict.add(Parameter.FTP_PORT,
                             r'port:(.*)',
                             lambda match: match.group(1),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="FTP Port",
                             description="Location on the OOI infrastructure where .raw files and echogram images will be stored.",
                             startup_param=True,
                             default_value=DEFAULT_PORT)

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_STATUS, display_name="Acquire Status")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # 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
        @retval next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Try to get the status to check if the instrument is alive
        host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
        port = self._param_dict.get_config_value(Parameter.FTP_PORT)
        response = self._url_request(host, port, '/status.json')

        if response is None:
            error_msg = "_handler_unknown_discover: Unable to connect to host: %s" % host
            log.error(error_msg)
            raise InstrumentConnectionException(error_msg)

        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        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_get(self, *args, **kwargs):
        """
        Get parameters while in the command state.
        @param params List of the parameters to pass to the state
        @retval returns (next_state, result) where result is a dict {}. No
            agent state changes happening with Get, so no next_agent_state
        @throw InstrumentParameterException for invalid parameter
        """
        result_vals = {}

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException('_handler_command_get requires a parameter dict.')

        if Parameter.ALL in params:
            log.debug("Parameter ALL in params")
            params = Parameter.list()
            params.remove(Parameter.ALL)

        log.debug("_handler_command_get: params = %s", params)

        if params is None or not isinstance(params, list):
            raise InstrumentParameterException("GET parameter list not a list!")

        # fill the return values from the update
        for param in params:
            if not Parameter.has(param):
                raise InstrumentParameterException("Invalid parameter!")
            result_vals[param] = self._param_dict.get(param)
            self._param_dict.get_config_value(param)
        result = result_vals

        log.debug("Get finished, next_state: %s, result: %s", None, result)
        return None, result

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        @retval next state, result
        """
        startup = False

        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('_handler_command_set: command requires a parameter dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        # For each key, val in the params, set the param dictionary.
        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, None

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        # verify param is not readonly param
        self._verify_not_readonly(*args, **kwargs)

        for key, val in params.iteritems():
            log.debug("KEY = %s VALUE = %s", key, val)
            self._param_dict.set_value(key, val)
            if key == Parameter.SCHEDULE:
                self._ftp_schedule_file()

                # Load the schedule file
                host = self._param_dict.get(Parameter.FTP_IP_ADDRESS)
                port = self._param_dict.get_config_value(Parameter.FTP_PORT)
                log.debug("_set_params: stop the current schedule file")
                self._url_request(host, port, '/stop_schedule', data={})
                log.debug("_set_params: upload driver YAML file to host %s", host)
                res = self._url_request(host, port, '/load_schedule', data=json.dumps({'filename': YAML_FILE_NAME}))
                log.debug("_set_params: result from load = %s", res)

        log.debug("set complete, update params")

    def _ftp_schedule_file(self):
        """
        Construct a YAML schedule file and
        ftp the file to the Instrument server
        """
        # Create a temporary file and write the schedule YAML information to the file
        try:
            config_file = tempfile.TemporaryFile()
            log.debug("temporary file created")

            if config_file is None or not isinstance(config_file, file):
                raise InstrumentException("config_file is not a temp file!")

            config_file.write(self._param_dict.get(Parameter.SCHEDULE))
            config_file.seek(0)
            log.debug("finished writing config file:\n%r", self._param_dict.get(Parameter.SCHEDULE))

        except Exception as e:
            log.error("Create schedule YAML file exception: %s", e)
            raise e

        #  FTP the schedule file to the ZPLSC server
        host = ''

        try:
            log.debug("Create a ftp session")
            host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
            log.debug("Got host ip address %s", host)

            ftp_session = ftplib.FTP()
            ftp_session.connect(host)
            ftp_session.login(USER_NAME, PASSWORD)
            log.debug("ftp session was created...")

            ftp_session.set_pasv(False)
            ftp_session.cwd("config")

            ftp_session.storlines('STOR ' + YAML_FILE_NAME, config_file)
            files = ftp_session.dir()

            log.debug("*** Config yaml file sent: %s", files)

            ftp_session.quit()
            config_file.close()

        except (ftplib.socket.error, ftplib.socket.gaierror), e:
            log.error("ERROR: cannot reach FTP Host %s: %s ", host, e)
            raise InstrumentException("ERROR: cannot reach FTP Host %s " % host)

        log.debug("*** FTP %s to ftp host %s successfully", YAML_FILE_NAME, host)
class Protocol(Pco2wProtocol):
    """
    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.
        """

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(
            ProtocolState, ProtocolEvent,
            ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Construct protocol superclass.
        Pco2wProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # build the chunker
        self._chunker = StringChunker(Protocol.sieve_function)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """

        return [x for x in events if Capability.has(x)]

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        :param raw_data: data to filter
        """

        return_list = []

        sieve_matchers = [SAMI_REGULAR_STATUS_REGEX_MATCHER,
                          SAMI_CONTROL_RECORD_REGEX_MATCHER,
                          PCO2W_SAMPLE_REGEX_MATCHER,
                          PCO2WA_CONFIGURATION_REGEX_MATCHER,
                          SAMI_ERROR_REGEX_MATCHER]

        for matcher in sieve_matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker. Pass it to
        extract_sample with the appropriate particle objects and REGEXes.
        """

        if any([self._extract_sample(SamiRegularStatusDataParticle, SAMI_REGULAR_STATUS_REGEX_MATCHER,
                                     chunk, timestamp),
                self._extract_sample(SamiControlRecordDataParticle, SAMI_CONTROL_RECORD_REGEX_MATCHER,
                                     chunk, timestamp),
                self._extract_sample(Pco2waConfigurationDataParticle, PCO2WA_CONFIGURATION_REGEX_MATCHER,
                                     chunk, timestamp)]):
            return

        sample = self._extract_sample(Pco2wSamiSampleDataParticle, PCO2W_SAMPLE_REGEX_MATCHER, chunk, timestamp)

        log.debug('Protocol._got_chunk(): get_current_state() == %s', self.get_current_state())

        if sample:
            self._verify_checksum(chunk, PCO2W_SAMPLE_REGEX_MATCHER)

    ########################################################################
    # Build Command, Driver and Parameter dictionaries
    ########################################################################

    def _build_param_dict(self):
        """
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        Pco2wProtocol._build_param_dict(self)

        ### example configuration string
        # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \
        #                       '000000000D000000000D000000000D07' + \
        #                       '1020FF54181C010038' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE
        #
        ###

        configuration_string_regex = self._get_configuration_string_regex()

        # Changed from 0x0A to 0x02 to indicate there is no external device, update IOS to indicate this is 0x02
        self._param_dict.add(Parameter.MODE_BITS, configuration_string_regex,
                             lambda match: int(match.group(4), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x02,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Mode Bits')

        ## Changed from 0x000E10 to 0x000000 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, configuration_string_regex,
                             lambda match: int(match.group(8), 16),
                             lambda x: self._int_to_hexstring(x, 6),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x000000,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Sample Interval')

        ## Changed from 0x01 to 0x00 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, configuration_string_regex,
                             lambda match: int(match.group(9), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x00,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Driver Version')

        ## Changed from 0x0B to 0x00 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, configuration_string_regex,
                             lambda match: int(match.group(10), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x00,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Parameter Pointer')

    ########################################################################
    # Overridden base class methods
    ########################################################################

    def _get_specific_configuration_string_parameters(self):
        """
        Overridden by device specific subclasses.
        """

        # An ordered list of parameters, can not use unordered dict
        # PCO2W driver extends the base class (SamiParameter)
        parameter_list = [Parameter.START_TIME_FROM_LAUNCH,
                          Parameter.STOP_TIME_FROM_START,
                          Parameter.MODE_BITS,
                          Parameter.SAMI_SAMPLE_INTERVAL,
                          Parameter.SAMI_DRIVER_VERSION,
                          Parameter.SAMI_PARAMS_POINTER,
                          Parameter.DEVICE1_SAMPLE_INTERVAL,
                          Parameter.DEVICE1_DRIVER_VERSION,
                          Parameter.DEVICE1_PARAMS_POINTER,
                          Parameter.DEVICE2_SAMPLE_INTERVAL,
                          Parameter.DEVICE2_DRIVER_VERSION,
                          Parameter.DEVICE2_PARAMS_POINTER,
                          Parameter.DEVICE3_SAMPLE_INTERVAL,
                          Parameter.DEVICE3_DRIVER_VERSION,
                          Parameter.DEVICE3_PARAMS_POINTER,
                          Parameter.PRESTART_SAMPLE_INTERVAL,
                          Parameter.PRESTART_DRIVER_VERSION,
                          Parameter.PRESTART_PARAMS_POINTER,
                          Parameter.GLOBAL_CONFIGURATION,
                          Parameter.PUMP_PULSE,
                          Parameter.PUMP_DURATION,
                          Parameter.SAMPLES_PER_MEASUREMENT,
                          Parameter.CYCLES_BETWEEN_BLANKS,
                          Parameter.NUMBER_REAGENT_CYCLES,
                          Parameter.NUMBER_BLANK_CYCLES,
                          Parameter.FLUSH_PUMP_INTERVAL,
                          Parameter.BIT_SWITCHES,
                          Parameter.NUMBER_EXTRA_PUMP_CYCLES]

        return parameter_list

    def _get_configuration_string_regex(self):
        """
        Get configuration string regex.
        @retval configuration string regex.
        """
        return PCO2WA_CONFIGURATION_REGEX

    def _get_configuration_string_regex_matcher(self):
        """
        Get config string regex matcher.
        @retval configuration string regex matcher
        """
        return PCO2WA_CONFIGURATION_REGEX_MATCHER
Beispiel #4
0
class SatlanticPARInstrumentProtocol(CommandResponseInstrumentProtocol):
    """The instrument protocol classes to deal with a Satlantic PAR sensor.
    The protocol is a very simple command/response protocol with a few show
    commands and a few set commands.
    Note protocol state machine must be called "self._protocol_fsm"
    """

    __metaclass__ = get_logging_metaclass(log_level='debug')

    def __init__(self, callback=None):
        CommandResponseInstrumentProtocol.__init__(self, Prompt, EOLN, callback)

        self._protocol_fsm = ThreadSafeFSM(PARProtocolState, PARProtocolEvent, PARProtocolEvent.ENTER, PARProtocolEvent.EXIT)

        self._protocol_fsm.add_handler(PARProtocolState.UNKNOWN, PARProtocolEvent.ENTER, self._handler_unknown_enter)
        self._protocol_fsm.add_handler(PARProtocolState.UNKNOWN, PARProtocolEvent.DISCOVER, self._handler_unknown_discover)

        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.ENTER, self._handler_command_enter)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.GET, self._handler_get)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.SET, self._handler_command_set)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.ACQUIRE_SAMPLE, self._handler_poll_acquire_sample)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.SCHEDULED_ACQUIRE_STATUS, self._handler_acquire_status)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(PARProtocolState.COMMAND, PARProtocolEvent.START_DIRECT, self._handler_command_start_direct)

        self._protocol_fsm.add_handler(PARProtocolState.AUTOSAMPLE, PARProtocolEvent.ENTER, self._handler_autosample_enter)
        self._protocol_fsm.add_handler(PARProtocolState.AUTOSAMPLE, PARProtocolEvent.GET, self._handler_get)
        self._protocol_fsm.add_handler(PARProtocolState.AUTOSAMPLE, PARProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample)
        self._protocol_fsm.add_handler(PARProtocolState.AUTOSAMPLE, PARProtocolEvent.SCHEDULED_ACQUIRE_STATUS, self._handler_autosample_acquire_status)

        self._protocol_fsm.add_handler(PARProtocolState.DIRECT_ACCESS, PARProtocolEvent.ENTER, self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(PARProtocolState.DIRECT_ACCESS, PARProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct)
        self._protocol_fsm.add_handler(PARProtocolState.DIRECT_ACCESS, PARProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct)

        self._protocol_fsm.start(PARProtocolState.UNKNOWN)

        self._add_response_handler(Commands.GET, self._parse_get_response)
        self._add_response_handler(Commands.SET, self._parse_set_response)
        self._add_response_handler(Commands.SAMPLE, self._parse_response)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_cmd_dict()
        self._build_driver_dict()

        self._param_dict.add(Parameter.MAXRATE,
                             MAXRATE_PATTERN,
                             lambda match: float(match.group(1)),
                             self._float_or_int_to_string,
                             direct_access=True,
                             startup_param=True,
                             init_value=4,
                             display_name='Max Rate',
                             description='Maximum sampling rate (0 (Auto) | 0.125 | 0.5 | 1 | 2 | 4 | 8 | 10 | 12)',
                             range={'Auto': 0, '0.125': 0.125, '0.5': 0.5, '1': 1, '2': 2, '4': 4, '8': 8, '10': 10,
                                    '12': 12},
                             type=ParameterDictType.FLOAT,
                             units=Units.HERTZ,
                             visibility=ParameterDictVisibility.READ_WRITE)

        self._param_dict.add(Parameter.SERIAL,
                             HEADER_PATTERN,
                             lambda match: match.group('sernum'),
                             str,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Serial Number',
                             description="",
                             type=ParameterDictType.STRING)

        self._param_dict.add(Parameter.FIRMWARE,
                             HEADER_PATTERN,
                             lambda match: match.group('firm'),
                             str,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Firmware Version',
                             description="",
                             type=ParameterDictType.STRING)

        self._param_dict.add(Parameter.ACQUIRE_STATUS_INTERVAL,
                             INTERVAL_TIME_REGEX,
                             lambda match: match.group(1),
                             str,
                             display_name="Acquire Status Interval",
                             description='Interval for gathering status particles.',
                             type=ParameterDictType.STRING,
                             units=ParameterUnits.TIME_INTERVAL,
                             visibility=ParameterDictVisibility.READ_WRITE,
                             default_value='00:00:00',
                             startup_param=True)

        self._chunker = StringChunker(SatlanticPARInstrumentProtocol.sieve_function)

        command_dict = Commands.dict()
        label_dict = CommandNames.dict()
        for key in command_dict:
            label = label_dict.get(key)
            command = command_dict[key]
            if command in [CommandNames.SET, CommandNames.GET]:
                command += ' '
            self._direct_commands[label] = command

    def _build_cmd_dict(self):
        """
        Build a command dictionary structure, load the strings for the metadata from a file if present.
        """
        self._cmd_dict = ProtocolCommandDict()
        self._cmd_dict.add(PARCapability.ACQUIRE_SAMPLE, display_name='Acquire Sample')
        self._cmd_dict.add(PARCapability.ACQUIRE_STATUS, display_name='Acquire Status')
        self._cmd_dict.add(PARCapability.START_AUTOSAMPLE, display_name='Start Autosample', timeout=40)
        self._cmd_dict.add(PARCapability.STOP_AUTOSAMPLE, display_name='Stop Autosample', timeout=40)
        self._cmd_dict.add(PARCapability.DISCOVER, display_name='Discover', timeout=50)

    def _build_driver_dict(self):
        """
        Build a driver dictionary structure, load the strings for the metadata from a file if present.
        """
        self._driver_dict = DriverDict()
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        """
        matchers = [SAMPLE_REGEX, MAXANDBAUDRATE_REGEX, HEADER_REGEX]
        return_list = []

        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))
                log.trace("sieve_function: regex found %r", raw_data[match.start():match.end()])

        return return_list

    def _filter_capabilities(self, events):
        """
        """
        events_out = [x for x in events if PARCapability.has(x)]
        return events_out

    def _do_cmd(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers.

        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @retval The fully built command that was sent
        @raises InstrumentProtocolException if command could not be built.
        """
        expected_prompt = kwargs.get('expected_prompt', None)
        cmd_line = self._build_default_command(cmd, *args)

        # Send command.
        if len(cmd_line) == 1:
            self._connection.send(cmd_line)
        else:
            for char in cmd_line:
                starttime = time.time()
                self._connection.send(char)
                while len(self._promptbuf) == 0 or char not in self._promptbuf[-1]:
                    time.sleep(0.0015)
                    if time.time() > starttime + 3:
                        break

            # Keep for reference: This is a reliable alternative, but not fully explained & may not work in the future.
            # It somehow corrects bit rate timing issues across the driver-digi-instrument network interface,
            # & allows the entire line of a commands to be sent successfully.
            if EOLN not in cmd_line:    # Note: Direct access commands may already include an EOLN
                time.sleep(0.115)
                starttime = time.time()
                self._connection.send(EOLN)
                while EOLN not in self._promptbuf[len(cmd_line):len(cmd_line) + 2] and Prompt.ENTER_EXIT_CMD_MODE \
                           not in self._promptbuf[len(cmd_line):len(cmd_line) + 2]:
                    time.sleep(0.0015)
                    if time.time() > starttime + 3:
                        break

            if cmd is Commands.EXIT:
                self._connection.send(EOLN)

        return cmd_line

    def _do_cmd_no_resp(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers. No response is handled as a result of the command.
        Overridden: special "write delay" & command resending
        reliability improvements, no need for wakeup, default build command used for all commands
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @raises InstrumentProtocolException if command could not be built.
        """
        self._do_cmd(cmd, *args, **kwargs)

    def _do_cmd_resp(self, cmd, *args, **kwargs):
        """
        Perform a command-response on the device. Overridden: special "write delay" & command resending
        reliability improvements, no need for wakeup, default build command used for all commands
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @param expected_prompt kwarg offering a specific prompt to look for
        other than the ones in the protocol class itself.
        @param response_regex kwarg with a compiled regex for the response to
        match. Groups that match will be returned as a string.
        Cannot be supplied with expected_prompt. May be helpful for instruments that do not have a prompt.
        @retval resp_result The (possibly parsed) response result including the
        first instance of the prompt matched. If a regex was used, the prompt
        will be an empty string and the response will be the joined collection of matched groups.
        @raises InstrumentTimeoutException if the response did not occur in time.
        @raises InstrumentProtocolException if command could not be built or if response was not recognized.
        """
        timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT)
        expected_prompt = kwargs.get('expected_prompt', None)
        response_regex = kwargs.get('response_regex', None)

        if response_regex and not isinstance(response_regex, RE_PATTERN):
            raise InstrumentProtocolException('Response regex is not a compiled pattern!')

        if expected_prompt and response_regex:
            raise InstrumentProtocolException('Cannot supply both regex and expected prompt!')

        retry_count = 5
        retry_num = 0
        cmd_line = ""
        result = ""
        prompt = ""
        for retry_num in xrange(retry_count):
            # Clear line and prompt buffers for result.
            self._linebuf = ''
            self._promptbuf = ''

            cmd_line = self._do_cmd(cmd, *args, **kwargs)

            # Wait for the prompt, prepare result and return, timeout exception
            if response_regex:
                result_tuple = self._get_response(timeout, response_regex=response_regex,
                                                  expected_prompt=expected_prompt)
                result = "".join(result_tuple)
            else:
                (prompt, result) = self._get_response(timeout, expected_prompt=expected_prompt)

            # Confirm the entire command was sent, otherwise resend retry_count number of times
            if len(cmd_line) > 1 and \
                (expected_prompt is not None or
                (response_regex is not None))\
                    and not result.startswith(cmd_line):
                    # and cmd_line not in result:
                log.debug("_do_cmd_resp: Send command: %r failed %r attempt, result = %r.", cmd, retry_num, result)
                if retry_num >= retry_count:
                    raise InstrumentCommandException('_do_cmd_resp: Failed %d attempts sending command: %r' %
                                                     (retry_count, cmd))
            else:
                break

        log.debug("_do_cmd_resp: Sent command: %r, %d reattempts, expected_prompt=%r, result=%r.",
                  cmd_line, retry_num, expected_prompt, result)

        resp_handler = self._response_handlers.get((self.get_current_state(), cmd), None) or \
            self._response_handlers.get(cmd, None)
        resp_result = None
        if resp_handler:
            resp_result = resp_handler(result, prompt)

        time.sleep(0.3)     # give some time for the instrument connection to keep up

        return resp_result

    ########################################################################
    # Unknown handlers.
    ########################################################################
    def _handler_unknown_enter(self):
        """
        Enter unknown state.
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_unknown_discover(self):
        """
        Discover current state; can be COMMAND or AUTOSAMPLE.
        """
        result = []
        try:
            probe_resp = self._do_cmd_resp(Commands.SAMPLE, timeout=2,
                                           expected_prompt=[Prompt.SAMPLES, PARProtocolError.INVALID_COMMAND])
        except InstrumentTimeoutException:
            self._do_cmd_resp(Commands.SWITCH_TO_AUTOSAMPLE, expected_prompt=Prompt.SAMPLES, timeout=15)
            next_state = PARProtocolState.AUTOSAMPLE
            return next_state, (next_state, result)

        if probe_resp == PARProtocolError.INVALID_COMMAND:
            next_state = PARProtocolState.COMMAND
        else:
            # Put the instrument into full autosample in case it isn't already (could be in polled mode)
            result = self._do_cmd_resp(Commands.SWITCH_TO_AUTOSAMPLE, expected_prompt=Prompt.SAMPLES, timeout=15)
            next_state = PARProtocolState.AUTOSAMPLE

        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_enter(self):
        """
        Enter command state.
        """
        # Command device to update parameters and send a config change event.
        if self._init_type != InitializationType.NONE:
            self._update_params()
            # we need to briefly start sampling so we can stop sampling
            # and get the serial number and firmware version
            self._do_cmd_no_resp(Commands.EXIT)
            self._send_break()

        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _update_params(self):
        """
        Fetch the parameters from the device, and update the param dict.
        """
        max_rate_response = self._do_cmd_resp(Commands.GET, Parameter.MAXRATE, expected_prompt=Prompt.COMMAND)
        self._param_dict.update(max_rate_response)

    def _set_params(self, params, startup=False, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        Also called when setting parameters during startup and direct access

        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.

        @param params dictionary containing parameter name and value
        @param startup bool True is we are initializing, False otherwise
        @raise InstrumentParameterException
        """
        # Retrieve required parameter from args.
        # Raise exception if no parameter provided, or not a dict.

        scheduling_interval_changed = False
        instrument_params_changed = False
        old_config = self._param_dict.get_all()

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set params requires a parameter dict.')

        self._verify_not_readonly(params, startup)

        if Parameter.ACQUIRE_STATUS_INTERVAL in params:

            old_val = self._param_dict.format(Parameter.ACQUIRE_STATUS_INTERVAL)
            new_val = self._param_dict.format(Parameter.ACQUIRE_STATUS_INTERVAL,
                                              params[Parameter.ACQUIRE_STATUS_INTERVAL])
            if old_val != new_val:
                valid_value_regex = r'^\d{2}:[0-5]\d:[0-5]\d$'
                range_checker = re.compile(valid_value_regex)
                if range_checker.match(new_val):
                    self._setup_scheduler_config(new_val)
                    self._param_dict.set_value(Parameter.ACQUIRE_STATUS_INTERVAL, new_val)
                else:
                    raise InstrumentParameterException("invalid time string parameter for acquire status interval")

        for name, value in params.iteritems():

            old_val = self._param_dict.format(name)
            new_val = self._param_dict.format(name, params[name])

            log.debug('Changing param %r OLD = %r, NEW %r', name, old_val, new_val)

            if name == Parameter.MAXRATE:
                if value not in VALID_MAXRATES:
                    raise InstrumentParameterException("Maxrate %s out of range" % value)

                if old_val != new_val:
                    if self._do_cmd_resp(Commands.SET, name, new_val, expected_prompt=Prompt.COMMAND):
                        instrument_params_changed = True
            elif name == Parameter.ACQUIRE_STATUS_INTERVAL:
                pass
            else:
                raise InstrumentParameterException("Parameter not in dictionary: %s" % name)

        if instrument_params_changed:
            self._do_cmd_resp(Commands.SAVE, expected_prompt=Prompt.COMMAND)
            self._update_params()

        new_config = self._param_dict.get_all()
        log.debug("Updated parameter dict: old_config = %r, new_config = %r", old_config, new_config)
        if new_config != old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        for name in params.keys():
            if self._param_dict.format(name, params[name]) != self._param_dict.format(name):
                raise InstrumentParameterException('Failed to update parameter: %r' % name)

    def _setup_scheduler_config(self, event_value):
        """
        Set up auto scheduler configuration.
        """
        try:
            interval = event_value.split(':')
            hours = int(interval[0])
            minutes = int(interval[1])
            seconds = int(interval[2])
            log.debug("Setting scheduled interval to: %s %s %s", hours, minutes, seconds)
        except(KeyError, ValueError) as e:
            log.debug("invalid value for acquire status interval: %r", e)
            raise InstrumentParameterException('invalid value for Acquire Status Interval: ' + event_value)

        if DriverConfigKey.SCHEDULER in self._startup_config:
            self._startup_config[DriverConfigKey.SCHEDULER][ScheduledJob.ACQUIRE_STATUS] = {
                DriverSchedulerConfigKey.TRIGGER: {
                    DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                    DriverSchedulerConfigKey.HOURS: int(hours),
                    DriverSchedulerConfigKey.MINUTES: int(minutes),
                    DriverSchedulerConfigKey.SECONDS: int(seconds)}
            }
        else:

            self._startup_config[DriverConfigKey.SCHEDULER] = {
                ScheduledJob.ACQUIRE_STATUS: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.HOURS: int(hours),
                        DriverSchedulerConfigKey.MINUTES: int(minutes),
                        DriverSchedulerConfigKey.SECONDS: int(seconds)}
                },
            }

        # Start the scheduler if it is not running
        if not self._scheduler:
            self.initialize_scheduler()

        # First remove the scheduler, if it exists
        if not self._scheduler_callback.get(ScheduledJob.ACQUIRE_STATUS) is None:
            self._remove_scheduler(ScheduledJob.ACQUIRE_STATUS)
            log.debug("Removed scheduler for acquire status")

        # Now Add the scheduler
        if hours > 0 or minutes > 0 or seconds > 0:
            self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, PARProtocolEvent.SCHEDULED_ACQUIRE_STATUS)

    def _handler_command_set(self, *args, **kwargs):
        """
        Handle setting data from command mode.
        @param params Dict of the parameters and values to pass to the state
        """
        next_state = None
        result = []
        self._set_params(*args, **kwargs)
        return next_state, result

    def _handler_command_start_autosample(self):
        """
        Handle getting a start autosample event when in command mode
        """
        next_state = PARProtocolState.AUTOSAMPLE
        result = []

        self._do_cmd_resp(Commands.EXIT, expected_prompt=Prompt.SAMPLES, timeout=15)
        time.sleep(0.115)
        self._do_cmd_resp(Commands.SWITCH_TO_AUTOSAMPLE, expected_prompt=Prompt.SAMPLES, timeout=15)

        return next_state, (next_state, result)

    def _handler_command_start_direct(self):
        next_state = PARProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################
    def _handler_autosample_enter(self):
        """
        Handle PARProtocolState.AUTOSAMPLE PARProtocolEvent.ENTER
        """
        next_state = None
        result = []

        if self._init_type != InitializationType.NONE:
            next_state, (_, result) = self._handler_autosample_stop_autosample()
            self._update_params()
            self._init_params()
            next_state, (_, result) = self._handler_command_start_autosample()

        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

        return next_state, (next_state, result)

    def _handler_autosample_stop_autosample(self):
        """
        Handle PARProtocolState.AUTOSAMPLE stop
        @throw InstrumentProtocolException For hardware error
        """
        next_state = PARProtocolState.COMMAND
        try:
            self._send_break()
            result = ['Autosample break successful, returning to command mode']
        except InstrumentException, e:
            log.debug("_handler_autosample_stop_autosample error: %s", e)
            raise InstrumentProtocolException(error_code=InstErrorCode.HARDWARE_ERROR,
                                              msg="Couldn't break from autosample!")

        return next_state, (next_state, result)
Beispiel #5
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_TURBO, self._handler_command_start_turbo),
            ],
            ProtocolState.SPINNING_UP: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.AT_SPEED, self._handler_spinning_up_at_speed),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.AT_SPEED: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.CLEAR, self._handler_clear),
                (ProtocolEvent.GET, self._handler_command_get),
            ],
            ProtocolState.SPINNING_DOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOPPED, self._handler_spinning_down_stopped),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # 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()

        # Add build and response handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_build_handler(command, self._generic_build_handler)
            self._add_response_handler(command, self._generic_response_handler)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(Protocol.sieve_function)
        self._max_current_count = 0
        self.initialize_scheduler()

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        @param raw_data: data to be searched
        @return: list of (start,stop) indexes of matches
        """
        return [(m.start(), m.end()) for m in TurboStatusParticle.regex_compiled().finditer(raw_data)]

    def _build_param_dict(self):
        """
        All turbo parameters have the same signature, add them in a loop...
        """
        parameters = {
            Parameter.UPDATE_INTERVAL: {
                'display_name': 'Acquire Status Interval',
                'description': 'Interval between automatic acquire status calls: (5 - 60)',
                'units': Units.SECOND,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_DRIVE_CURRENT: {
                'display_name': 'Maximum Allowable Drive Current',
                'description': 'Maximum allowable drive current at speed: (100 - 200)',
                'units': Prefixes.CENTI + Units.AMPERE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_TEMP_MOTOR: {
                'display_name': 'Maximum Allowable Motor Temperature',
                'description': 'Maximum allowable motor temperature: (5 - 100)',
                'units': Units.DEGREE_CELSIUS,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_TEMP_BEARING: {
                'display_name': 'Maximum Allowable Bearing Temperature',
                'description': 'Maximum allowable bearing temperature: (5 - 100)',
                'units': Units.DEGREE_CELSIUS,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MIN_SPEED: {
                'display_name': 'Minimum Allowable Turbo Speed',
                'description': 'Minimum allowable turbo speed before RGA is shutdown: (70000 - 90000)',
                'units': Units.REVOLUTION_PER_MINUTE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.TARGET_SPEED: {
                'display_name': 'Target Turbo Speed',
                'description': 'Target turbo speed before RGA is initialized: (70000 - 90000)',
                'units': Units.REVOLUTION_PER_MINUTE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.ERROR_REASON: {
                'display_name': 'Turbo Error Reason',
                'description': 'Reason for turbo error state.',
                'visibility': ParameterDictVisibility.READ_ONLY,
                'type': ParameterDictType.STRING,
            }
        }

        reverse_param = Parameter.reverse_dict()
        constraints = ParameterConstraints.dict()

        for name in parameters:
            kwargs = parameters[name]
            if name in constraints:
                _type, minimum, maximum = constraints[name]
                kwargs['val_description'] = '%s value from %d - %d' % (_type, minimum, maximum)
            self._param_dict.add(name, '', None, None, **kwargs)

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.ACQUIRE_STATUS, display_name="Acquire Status")
        self._cmd_dict.add(Capability.START_TURBO, display_name="Start Turbo")
        self._cmd_dict.add(Capability.STOP_TURBO, display_name="Stop Turbo")
        self._cmd_dict.add(Capability.CLEAR, display_name="Clear Error State")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _got_chunk(self, chunk, ts):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and regexes.
        @param chunk: data to be processed
        @param ts: timestamp
        """
        self._extract_sample(TurboStatusParticle, TurboStatusParticle.regex_compiled(), chunk, ts)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events: events to be filtered
        @return: list of events that are in Capability
        """
        return [x for x in events if Capability.has(x)]

    @staticmethod
    def _checksum(s):
        """
        Calculate the turbopump checksum for the given string.
        @param s: string to be checked
        @return: checksum string
        """
        return '%03d' % (sum([ord(x) for x in s]) % 256)

    def _build_turbo_command(self, address, c_type, c, data):
        """
        Build a command for the turbopump
        @param address: target address
        @param c_type: command type (QUERY/SET)
        @param c: command
        @param data: command_data
        @return: command string
        """
        command = '%03d%02d%03d%02d%s' % (address, c_type, c, len(data), data)
        checksum = self._checksum(command)
        return command + checksum

    def _generic_build_handler(self, command, *args, **kwargs):
        """
        Determine if this is a query or set action based on the
        input args.  Dispatch the builder with the appropriate arguments.
        @param command: command to be sent
        @param args: arglist which may contain a value
        @return: command string
        """
        if len(args) == 1:
            # this is a set action
            value = args[0]
            return self._build_turbo_command(ADDRESS, CommandType.SET, command, value) + NEWLINE
        # this is a query
        return self._build_turbo_command(ADDRESS, CommandType.QUERY, command, QUERY) + NEWLINE

    def _generic_response_handler(self, resp, prompt):
        """
        Parse the response from the turbopump.
        @param resp: response
        @param prompt: unused, require to match signature
        @returns: integer value extracted from response
        @throws InstrumentDataException
        """
        my_checksum = self._checksum(resp[:-3])
        if resp[-3:] != my_checksum:
            err_str = 'bad checksum: %r calculated: %r' % (resp, my_checksum)
            raise exceptions.InstrumentDataException(err_str)
        command = int(resp[5:8])
        data_length = int(resp[8:10])
        data = resp[10:-3]
        log.trace('command: %s data: %s', command, data)
        if len(data) != data_length:
            raise exceptions.InstrumentDataException('invalid data length: %r' % resp)
        if command not in InstrumentCommand.list():
            raise exceptions.InstrumentDataException('command not found: %r' % resp)
        return int(data)

    def _wakeup(self, timeout, delay=1):
        """
        Not valid for this instrument
        """

    def _build_scheduler(self):
        """
        Build a scheduler for periodic status updates
        """
        job_name = ScheduledJob.ACQUIRE_STATUS
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.UPDATE_INTERVAL)
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, ProtocolEvent.ACQUIRE_STATUS)

    def _update_params(self, *args, **kwargs):
        """
        Parameters are NOT set in the instrument by this method, as all parameters are driver only.
        """

    def _set_params(self, *args, **kwargs):
        """
        Set parameters, raise a CONFIG_CHANGE event if necessary.
        @throws InstrumentParameterException
        """
        self._verify_not_readonly(*args, **kwargs)
        params_to_set = args[0]
        old_config = self._param_dict.get_all()

        # check if in range
        constraints = ParameterConstraints.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params_to_set.iteritems():
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                try:
                    value = var_type(val)
                except ValueError:
                    raise exceptions.InstrumentParameterException(
                        'Unable to verify type - parameter: %s value: %s' % (key, val))
                if val < minimum or val > maximum:
                    raise exceptions.InstrumentParameterException(
                        'Value out of range - parameter: %s value: %s min: %s max: %s' %
                        (key, val, minimum, maximum))

        # all constraints met or no constraints exist, set the values
        for key, val in params_to_set.iteritems():
            if key in old_config:
                self._param_dict.set_value(key, val)
            else:
                raise exceptions.InstrumentParameterException(
                    'Attempted to set unknown parameter: %s value: %s' % (key, val))
        new_config = self._param_dict.get_all()

        # If we changed anything, raise a CONFIG_CHANGE event
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _send_command_with_retry(self, command, value=None, sleep_time=1, max_retries=MAX_RETRIES):
        """
        Attempt to send a command up to max_retries times.  Protocol state will move to ERROR if we fail to
        receive a response after max_retries attempts.
        @throws InstrumentTimeoutException
        """
        for attempt in xrange(1, max_retries + 1):
            try:
                if value is None:
                    result = self._do_cmd_resp(command, response_regex=TURBO_RESPONSE, timeout=TIMEOUT)
                else:
                    result = self._do_cmd_resp(command, value, response_regex=TURBO_RESPONSE, timeout=TIMEOUT)
                return result
            except exceptions.InstrumentTimeoutException:
                log.error('Error sending command: %s, attempt %d', command, attempt)
                time.sleep(sleep_time)

        # set the error reason
        self._param_dict.set_value(Parameter.ERROR_REASON, 'Unable to command the turbo')
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        self._async_raise_fsm_event(ProtocolEvent.ERROR)
        raise exceptions.InstrumentTimeoutException('Failed to command the turbo: %s' % command)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler when no specific action is needed.
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler when no specific action is needed.
        """

    def _handler_acquire_status(self, *args, **kwargs):
        """
        Query the instrument for the following status items:
            drive current
            drive voltage
            bearing temp
            motor temp
            rotation speed

        Verify no values exceed the limits specified in the parameter dictionary.
        @returns: next_state, (next_agent_state, result)
        @throws InstrumentStateException
        """
        next_state = None
        responses = {}

        # query the turbo for the speed/temp/current values
        for command in [InstrumentCommand.DRIVE_CURRENT, InstrumentCommand.DRIVE_VOLTAGE,
                        InstrumentCommand.TEMP_BEARING, InstrumentCommand.TEMP_MOTOR,
                        InstrumentCommand.ROTATION_SPEED_ACTUAL]:
            responses[command] = self._send_command_with_retry(command)

        # check the current driver state
        current_state = self.get_current_state()
        error = None

        # Check for over temperature conditions
        if responses[InstrumentCommand.TEMP_MOTOR] > self._param_dict.get(Parameter.MAX_TEMP_MOTOR) or \
                responses[InstrumentCommand.TEMP_BEARING] > self._param_dict.get(Parameter.MAX_TEMP_BEARING):
            error = 'Over temp error - Motor: %d Bearing: %d' % (responses[InstrumentCommand.TEMP_MOTOR],
                                                                 responses[InstrumentCommand.TEMP_BEARING])

        # Check if we were up to speed but have dipped below MIN_SPEED
        elif current_state == ProtocolState.AT_SPEED:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] < self._param_dict.get(Parameter.MIN_SPEED):
                error = 'Fell below min speed: %d' % responses[InstrumentCommand.ROTATION_SPEED_ACTUAL]

            # or if we're up to speed and we have exceeded MAX_DRIVE_CURRENT more than 3 subsequent intervals
            if responses[InstrumentCommand.DRIVE_CURRENT] > self._param_dict.get(Parameter.MAX_DRIVE_CURRENT):
                self._max_current_count += 1
                if self._max_current_count > CURRENT_STABILIZE_RETRIES:
                    error = 'Turbo current draw to high: %d' % responses[InstrumentCommand.DRIVE_CURRENT]
            else:
                self._max_current_count = 0

        if error:
            self._param_dict.set_value(Parameter.ERROR_REASON, error)
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
            self._driver_event(DriverAsyncEvent.ERROR, error)

        # now check if up to speed when spinning up
        elif current_state == ProtocolState.SPINNING_UP:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] >= self._param_dict.get(Parameter.TARGET_SPEED):
                self._async_raise_fsm_event(ProtocolEvent.AT_SPEED)

        # or maybe we've stopped while spinning down (we'll consider < MIN_SPEED as stopped...)
        elif current_state == ProtocolState.SPINNING_DOWN:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] <= self._param_dict.get(Parameter.MIN_SPEED):
                self._async_raise_fsm_event(ProtocolEvent.STOPPED)

        return next_state, (next_state, responses)

    def _handler_stop_turbo(self):
        """
        Stop the turbo
        """
        next_state = ProtocolState.SPINNING_DOWN
        result = []
        for command in [InstrumentCommand.PUMP_STATION, InstrumentCommand.MOTOR_PUMP]:
            self._send_command_with_retry(command, value=FALSE)

        return next_state, (next_state, result)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state.  This instrument always discovers to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        self._init_params()

        # delete the scheduled acquire status job, if it exists.
        # This portion of the MASSP is powered OFF the majority of the time
        # so acquire_status should not be running
        try:
            self._remove_scheduler(ScheduledJob.ACQUIRE_STATUS)
        except KeyError:
            pass

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameter
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        """
        next_state = None
        result = None
        self._set_params(*args, **kwargs)

        return next_state, result

    def _handler_command_start_direct(self):
        """
        Start direct access
        """
        next_state = ProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    def _handler_command_start_turbo(self):
        """
        Start the turbo, periodic status scheduler
        """
        next_state = ProtocolState.SPINNING_UP
        result = []
        for command in [InstrumentCommand.PUMP_STATION, InstrumentCommand.MOTOR_PUMP]:
            self._send_command_with_retry(command, value=TRUE)
        # start the acquire_status scheduler
        self._build_scheduler()
        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_execute_direct(self, data):
        """
        Forward a direct access command to the instrument
        """
        next_state = None
        result = []
        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return next_state, (next_state, result)

    def _handler_direct_access_stop_direct(self):
        """
        Stop direct access, return to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Spinning up/down handlers.
    ########################################################################

    def _handler_spinning_up_at_speed(self):
        """
        Instrument has reached operating speed, transition states.
        """
        next_state = ProtocolState.AT_SPEED
        result = []
        return next_state, (next_state, result)

    def _handler_spinning_down_stopped(self):
        """
        Instrument has spun down, transition states.
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._async_agent_state_change(ResourceAgentState.COMMAND)
        return next_state, (next_state, result)

    ########################################################################
    # Error handlers.
    ########################################################################

    def _handler_error(self, *args, **kwargs):
        """
        Error detected, go to the ERROR state.
        """
        next_state = ProtocolState.ERROR
        result = []
        return next_state, (next_state, result)

    def _handler_clear(self, *args, **kwargs):
        """
        User requests error state be cleared, go to COMMAND.
        @returns: next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._param_dict.set_value(Parameter.ERROR_REASON, '')
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return next_state, (next_state, result)
Beispiel #6
0
class SBE43Protocol(SBE16Protocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    def __init__(self, prompts, newline, driver_event):
        """
        SBE43Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The SBE43 newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # Build SBE19 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_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND:
            [(ProtocolEvent.ENTER, self._handler_command_enter),
             (ProtocolEvent.EXIT, self._handler_generic_exit),
             (ProtocolEvent.ACQUIRE_SAMPLE,
              self._handler_command_acquire_sample),
             (ProtocolEvent.START_AUTOSAMPLE,
              self._handler_command_start_autosample),
             (ProtocolEvent.GET, self._handler_get),
             (ProtocolEvent.SET, self._handler_command_set),
             (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
             (ProtocolEvent.CLOCK_SYNC,
              self._handler_command_clock_sync_clock),
             (ProtocolEvent.ACQUIRE_STATUS,
              self._handler_command_acquire_status)],
            ProtocolState.ACQUIRING_SAMPLE: [
                (ProtocolEvent.ENTER, self._handler_acquiring_sample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_SAMPLE_ASYNC,
                 self._handler_acquire_sample_async),
            ],
            ProtocolState.DIRECT_ACCESS:
            [(ProtocolEvent.ENTER, self._handler_direct_access_enter),
             (ProtocolEvent.EXIT, self._handler_generic_exit),
             (ProtocolEvent.EXECUTE_DIRECT,
              self._handler_direct_access_execute_direct),
             (ProtocolEvent.STOP_DIRECT,
              self._handler_direct_access_stop_direct)],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.STOP_AUTOSAMPLE,
                 self._handler_autosample_stop_autosample),
                (ProtocolEvent.SCHEDULED_ACQUIRED_STATUS,
                 self._handler_autosample_acquire_status),
            ]
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add build handlers for device commands, only using simple command handler.
        for cmd in Command.list():
            if cmd == Command.SET:
                self._add_build_handler(Command.SET, self._build_set_command)
            else:
                self._add_build_handler(cmd, self._build_simple_command)

        # Add response handlers for device commands.
        # these are here to ensure that correct responses to the commands are received before the next command is sent
        self._add_response_handler(Command.SET, self._parse_set_response)
        self._add_response_handler(Command.GET_SD,
                                   self._validate_GetSD_response)
        self._add_response_handler(Command.GET_HD,
                                   self._validate_GetHD_response)
        self._add_response_handler(Command.GET_CD,
                                   self._validate_GetCD_response)
        self._add_response_handler(Command.GET_CC,
                                   self._validate_GetCC_response)
        self._add_response_handler(Command.GET_EC,
                                   self._validate_GetEC_response)

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(self.sieve_function)

    def _build_command_dict(self):
        """
        Populate the command dictionary with command. Overridden to specify timeouts.
        """
        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,
                           timeout=ACQUIRE_STATUS_TIMEOUT,
                           display_name="Acquire Status")
        self._cmd_dict.add(Capability.ACQUIRE_SAMPLE,
                           display_name="Acquire Sample")
        self._cmd_dict.add(Capability.DISCOVER,
                           timeout=DISCOVER_TIMEOUT,
                           display_name='Discover')

    def _filter_capabilities(self, events):
        return [x for x in events if Capability.has(x)]

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        Over-ride sieve function to handle additional particles.
        """
        matchers = []
        return_list = []

        matchers.append(SBE43DataParticle.regex_compiled())
        matchers.append(SBE43HardwareParticle.regex_compiled())
        matchers.append(SBE43CalibrationParticle.regex_compiled())
        matchers.append(SBE43StatusParticle.regex_compiled())
        matchers.append(SBE43ConfigurationParticle.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):
        """
        Over-ride sieve function to handle additional particles.
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        if self._extract_sample(SBE43DataParticle,
                                SBE43DataParticle.regex_compiled(), chunk,
                                timestamp):
            self._sampling = True
            return

        for particle_class in SBE43HardwareParticle, \
                              SBE43CalibrationParticle, \
                              SBE43ConfigurationParticle, \
                              SBE43StatusParticle:
            if self._extract_sample(particle_class,
                                    particle_class.regex_compiled(), chunk,
                                    timestamp):
                return

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        self._verify_not_readonly(*args, **kwargs)
        update_params = False

        # check values that the instrument doesn't validate
        # handle special cases for driver specific parameters
        for (key, val) in params.iteritems():
            if key == Parameter.PUMP_DELAY and (val < MIN_PUMP_DELAY
                                                or val > MAX_PUMP_DELAY):
                raise InstrumentParameterException("pump delay out of range")
            elif key == Parameter.NUM_AVG_SAMPLES and (val < MIN_AVG_SAMPLES or
                                                       val > MAX_AVG_SAMPLES):
                raise InstrumentParameterException(
                    "num average samples out of range")

        for (key, val) in params.iteritems():

            old_val = self._param_dict.format(key)
            new_val = self._param_dict.format(key, val)
            log.debug("KEY = %r OLD VALUE = %r NEW VALUE = %r", key, old_val,
                      new_val)

            if old_val != new_val:
                update_params = True
                if ConfirmedParameter.has(key):
                    # We add a write delay here because this command has to be sent
                    # twice, the write delay allows it to process the first command
                    # before it receives the beginning of the second.
                    self._do_cmd_resp(Command.SET, key, val, write_delay=0.2)
                else:
                    self._do_cmd_resp(Command.SET, key, val, **kwargs)

        log.debug("set complete, update params")
        if update_params:
            self._update_params()

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_acquire_status(self, *args, **kwargs):
        """
        Get device status
        """
        next_state = None

        response = self._do_cmd_resp(
            Command.GET_SD,
            response_regex=SBE43StatusParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetSD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_HD,
            response_regex=SBE43HardwareParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetHD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_CD,
            response_regex=SBE43ConfigurationParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetCD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_CC,
            response_regex=SBE43CalibrationParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetCC Response: %s",
                  response)
        response = self._do_cmd_resp(Command.GET_EC, timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetEC Response: %s",
                  response)

        # Reset the event counter right after getEC
        self._do_cmd_resp(Command.RESET_EC, timeout=TIMEOUT)

        result = self.wait_for_particles([
            DataParticleType.DEVICE_STATUS,
            DataParticleType.DEVICE_CALIBRATION,
            DataParticleType.DEVICE_HARDWARE,
            DataParticleType.DEVICE_CONFIGURATION
        ])

        return next_state, (next_state, result)

    def _handler_command_acquire_sample(self, *args, **kwargs):
        """
        Acquire Sample is implemented asynchronously. Transition to ACQUIRING_SAMPLE state.
        """
        next_state = ProtocolState.ACQUIRING_SAMPLE
        result = []

        return next_state, (next_state, result)

    def _handler_acquiring_sample_enter(self):
        """
        Trigger the ACQUIRE_SAMPLE_ASYNC event
        """
        self._async_raise_fsm_event(ProtocolEvent.ACQUIRE_SAMPLE_ASYNC)

    def _handler_acquire_sample_async(self, *args, **kwargs):
        """
        Acquire sample from SBE16.
        @retval next_state, (next_state, result) tuple
        """
        next_state = ProtocolState.COMMAND
        result = []

        self._do_cmd_resp(Command.TS, *args, timeout=ASYNC_TIMEOUT, **kwargs)

        return next_state, (next_state, result)

    def _handler_autosample_acquire_status(self, *args, **kwargs):
        """
        Get device status in autosample mode
        """
        next_state = None
        result = []

        # When in autosample this command requires two wake-ups to get to the right prompt
        self._wakeup(timeout=WAKEUP_TIMEOUT, delay=0.3)
        self._wakeup(timeout=WAKEUP_TIMEOUT, delay=0.3)

        response = self._do_cmd_resp(
            Command.GET_SD,
            response_regex=SBE43StatusParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetSD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_HD,
            response_regex=SBE43HardwareParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetHD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_CD,
            response_regex=SBE43ConfigurationParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetCD Response: %s",
                  response)
        response = self._do_cmd_resp(
            Command.GET_CC,
            response_regex=SBE43CalibrationParticle.regex_compiled(),
            timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetCC Response: %s",
                  response)
        response = self._do_cmd_resp(Command.GET_EC, timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetEC Response: %s",
                  response)

        # Reset the event counter right after getEC
        self._do_cmd_no_resp(Command.RESET_EC)

        return next_state, (next_state, result)

    ########################################################################
    # response handlers.
    ########################################################################
    def _validate_GetSD_response(self, response, prompt):
        """
        validation handler for GetSD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error(
                "_validate_GetSD_response: GetSD command encountered error; type='%s' msg='%s'",
                error[0], error[1])
            raise InstrumentProtocolException(
                'GetSD command failure: type="%s" msg="%s"' %
                (error[0], error[1]))

        if not SBE43StatusParticle.resp_regex_compiled().search(response):
            log.error(
                '_validate_GetSD_response: GetSD command not recognized: %s.' %
                response)
            raise InstrumentProtocolException(
                'GetSD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetHD_response(self, response, prompt):
        """
        validation handler for GetHD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetHD command encountered error; type='%s' msg='%s'",
                      error[0], error[1])
            raise InstrumentProtocolException(
                'GetHD command failure: type="%s" msg="%s"' %
                (error[0], error[1]))

        if not SBE43HardwareParticle.resp_regex_compiled().search(response):
            log.error(
                '_validate_GetHD_response: GetHD command not recognized: %s.' %
                response)
            raise InstrumentProtocolException(
                'GetHD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetCD_response(self, response, prompt):
        """
        validation handler for GetCD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetCD command encountered error; type='%s' msg='%s'",
                      error[0], error[1])
            raise InstrumentProtocolException(
                'GetCD command failure: type="%s" msg="%s"' %
                (error[0], error[1]))

        if not SBE43ConfigurationParticle.resp_regex_compiled().search(
                response):
            log.error(
                '_validate_GetCD_response: GetCD command not recognized: %s.' %
                response)
            raise InstrumentProtocolException(
                'GetCD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetCC_response(self, response, prompt):
        """
        validation handler for GetCC command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetCC command encountered error; type='%s' msg='%s'",
                      error[0], error[1])
            raise InstrumentProtocolException(
                'GetCC command failure: type="%s" msg="%s"' %
                (error[0], error[1]))

        if not SBE43CalibrationParticle.resp_regex_compiled().search(response):
            log.error(
                '_validate_GetCC_response: GetCC command not recognized: %s.' %
                response)
            raise InstrumentProtocolException(
                'GetCC command not recognized: %s.' % response)

        return response

    def _validate_GetEC_response(self, response, prompt):
        """
        validation handler for GetEC command
        @param response command response string.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetEC command encountered error; type='%s' msg='%s'",
                      error[0], error[1])
            raise InstrumentProtocolException(
                'GetEC command failure: type="%s" msg="%s"' %
                (error[0], error[1]))

        return response

    ########################################################################
    # Private helpers.
    ########################################################################
    def _build_param_dict(self):
        """
        Populate the parameter dictionary with SBE19 parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        self._build_common_param_dict()

        self._param_dict.add(Parameter.SBE63,
                             r'SBE63>(.*)</SBE63',
                             lambda match: True
                             if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="SBE63 Attached",
                             range={
                                 'True': True,
                                 'False': False
                             },
                             description="Enable SBE63: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.NUM_AVG_SAMPLES,
                             r'ScansToAverage>([\d]+)</ScansToAverage>',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.INT,
                             display_name="Scans to Average",
                             range=INT16,
                             description="Number of samples to average",
                             startup_param=True,
                             direct_access=False,
                             default_value=4,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self._param_dict.add(
            Parameter.MIN_COND_FREQ,
            r'MinimumCondFreq>([\d]+)</MinimumCondFreq',
            lambda match: int(match.group(1)),
            str,
            type=ParameterDictType.INT,
            display_name="Minimum Conductivity Frequency",
            range=INT16,
            description=
            "Minimum conductivity frequency to enable pump turn-on.",
            startup_param=True,
            direct_access=False,
            default_value=500,
            units=Units.HERTZ,
            visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(
            Parameter.PUMP_DELAY,
            r'PumpDelay>([\d]+)</PumpDelay',
            lambda match: int(match.group(1)),
            str,
            type=ParameterDictType.INT,
            display_name="Pump Delay",
            range=INT16,
            description=
            "Time to wait after minimum conductivity frequency is reached before turning pump on.",
            startup_param=True,
            direct_access=False,
            default_value=60,
            units=Units.SECOND,
            visibility=ParameterDictVisibility.READ_WRITE)
        self._param_dict.add(
            Parameter.AUTO_RUN,
            r'AutoRun>(.*)</AutoRun',
            lambda match: True if match.group(1) == 'yes' else False,
            self._true_false_to_string,
            type=ParameterDictType.BOOL,
            display_name="Auto Run",
            range={
                'True': True,
                'False': False
            },
            description=
            "Enable automatic logging when power is applied: (true | false).",
            startup_param=True,
            direct_access=True,
            default_value=False,
            visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(
            Parameter.IGNORE_SWITCH,
            r'IgnoreSwitch>(.*)</IgnoreSwitch',
            lambda match: True if match.group(1) == 'yes' else False,
            self._true_false_to_string,
            type=ParameterDictType.BOOL,
            display_name="Ignore Switch",
            range={
                'True': True,
                'False': False
            },
            description=
            "Disable magnetic switch position for starting or stopping logging: (true | false)",
            startup_param=True,
            direct_access=True,
            default_value=True,
            visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(
            Parameter.PTYPE,
            r"<Sensor id = 'Main Pressure'>.*?<type>(.*?)</type>.*?</Sensor>",
            self._pressure_sensor_to_int,
            str,
            type=ParameterDictType.INT,
            display_name="Pressure Sensor Type",
            range={
                'Strain Gauge': 1,
                'Quartz with Temp Comp': 3
            },
            startup_param=True,
            direct_access=True,
            default_value=1,
            description=
            "Sensor type: (1:strain gauge | 3:quartz with temp comp)",
            visibility=ParameterDictVisibility.IMMUTABLE,
            regex_flags=re.DOTALL)
        self._param_dict.add(Parameter.OPTODE,
                             r'OPTODE>(.*)</OPTODE',
                             lambda match: True
                             if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="Optode Attached",
                             range={
                                 'True': True,
                                 'False': False
                             },
                             description="Enable optode: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(
            Parameter.VOLT1,
            r'ExtVolt1>(.*)</ExtVolt1',
            lambda match: True if match.group(1) == 'yes' else False,
            self._true_false_to_string,
            type=ParameterDictType.BOOL,
            display_name="Volt 1",
            range={
                'True': True,
                'False': False
            },
            description="Enable external voltage 1: (true | false)",
            startup_param=True,
            direct_access=True,
            default_value=False,
            visibility=ParameterDictVisibility.IMMUTABLE)
class THSPHProtocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    SERIES_A = 'A'
    SERIES_B = 'B'
    SERIES_C = 'C'
    GET_SAMPLE_SERIES_A = 'aH*'  # Gets data sample from ADC for series A
    GET_SAMPLE_SERIES_B = 'bH*'  # Gets data sample from ADC for series B
    GET_SAMPLE_SERIES_C = 'cH*'  # Gets data sample from ADC for series C

    # THSPH commands for instrument series A, B and C
    THSPH_COMMANDS = {
        SERIES_A: {Command.GET_SAMPLE: GET_SAMPLE_SERIES_A},
        SERIES_B: {Command.GET_SAMPLE: GET_SAMPLE_SERIES_B},
        SERIES_C: {Command.GET_SAMPLE: GET_SAMPLE_SERIES_C},
    }

    __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.
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover)

        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_AUTOSAMPLE,
                                       self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ACQUIRE_SAMPLE,
                                       self._handler_command_acquire_sample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT,
                                       self._handler_command_start_direct)

        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.SCHEDULE_ACQUIRE_SAMPLE,
                                       self._handler_command_acquire_sample)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE,
                                       self._handler_autosample_stop_autosample)

        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)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add build handlers for device commands.
        self._add_build_handler(Command.GET_SAMPLE, self._build_simple_command)

        # State state machine in COMMAND state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(THSPHProtocol.sieve_function)

        # Set Get Sample Command and Communication Test Command for Series A as default
        self._get_sample_cmd = self.GET_SAMPLE_SERIES_A

        self._direct_commands['Newline'] = self._newline
        self._direct_commands['Test A'] = 'aP*' + self._newline
        self._direct_commands['Test B'] = 'bP*' + self._newline
        self._direct_commands['Test C'] = 'cP*' + self._newline
        self._direct_commands['Sample A'] = self.GET_SAMPLE_SERIES_A + self._newline
        self._direct_commands['Sample B'] = self.GET_SAMPLE_SERIES_B + self._newline
        self._direct_commands['Sample C'] = self.GET_SAMPLE_SERIES_C + self._newline

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        """
        matchers = []
        return_list = []

        matchers.append(THSPHParticle.regex_compiled())

        for matcher in matchers:
            log.trace('matcher: %r raw_data: %r', matcher.pattern, raw_data)
            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.
        """
        if not self._extract_sample(THSPHParticle, THSPHParticle.regex_compiled(), chunk, timestamp):
            raise InstrumentProtocolException("Unhandled chunk")

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_SAMPLE, display_name="Acquire Sample")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with THSPH parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        # Add parameter handlers to parameter dict.
        self._param_dict.add(Parameter.INTERVAL,
                             r'Auto Polled Interval = (\d+)',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.INT,
                             units=Units.SECOND,
                             display_name="Polled Interval",
                             range=(1, 600),
                             description="Polling interval, internal to driver (1-600).",
                             visibility=ParameterDictVisibility.READ_WRITE,
                             startup_param=True,
                             direct_access=False,
                             default_value=5)

        self._param_dict.add(Parameter.INSTRUMENT_SERIES,
                             r'Instrument Series = ([A-C])',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.STRING,
                             display_name="Instrument Series",
                             range={'A': 'A', 'B': 'B', 'C': 'C'},
                             description='Defines instance of instrument series [A, B, C].',
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=False,
                             default_value='A')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # Unknown State handlers.
    ########################################################################
    # noinspection PyUnusedLocal
    def _handler_unknown_enter(self, *args, **kwargs):

        # 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

    # noinspection PyUnusedLocal, PyMethodMayBeStatic
    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; Change next state to be COMMAND state.
        """
        next_state = ProtocolState.COMMAND
        result = []

        return next_state, (next_state, result)

    ########################################################################
    # Command State handlers.
    ########################################################################
    # noinspection PyUnusedLocal
    def _handler_command_acquire_sample(self, *args, **kwargs):
        """
        Get device status
        """
        timeout = time.time() + TIMEOUT

        next_state = None

        self._do_cmd_no_resp(Command.GET_SAMPLE, timeout=TIMEOUT)

        particles = self.wait_for_particles([DataParticleType.THSPH_PARSED], timeout)

        return next_state, (next_state, particles)

    # noinspection PyUnusedLocal
    def _handler_command_enter(self, *args, **kwargs):
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        pass

    def _handler_command_get(self, *args, **kwargs):
        """
        Get device parameters from the parameter dict.  First we set a baseline timestamp
        that all data expirations will be calculated against.  Then we try to get parameter
        value.  If we catch an expired parameter then we will update all parameters and get
        values using the original baseline time that we set at the beginning of this method.
        Assuming our _update_params is updating all parameter values properly then we can
        ensure that all data will be fresh.  Nobody likes stale data!
        @param args[0] list of parameters to retrieve, or DriverParameter.ALL.
        """
        next_state, result = self._handler_get(*args, **kwargs)
        # TODO - update return signature to match other handlers - next_state, (next_state, result)
        return next_state, result

    # noinspection PyUnusedLocal
    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @retval (next_state, result) tuple, (None, None).
        @throws InstrumentParameterException if missing set parameters, if set parameters not ALL and
        not a dict, or if parameter can't be properly formatted.
        """
        next_state = None
        result = []
        startup = False

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return next_state, result

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters internally to the driver. No issuing commands to the
        instrument needed for this driver.
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        # list can be null, like in the case of direct access params, in this case do nothing
        if not params:
            return

        # Do a range check before we start all sets
        for (key, val) in params.iteritems():

            if key == Parameter.INTERVAL and not (0 < val < 601):
                log.debug("Auto Sample Interval not in 1 to 600 range ")
                raise InstrumentParameterException("sample interval out of range [1, 600]")

            if key == Parameter.INSTRUMENT_SERIES:
                if val not in 'ABC':
                    log.debug("Instrument Series is not A, B or C ")
                    raise InstrumentParameterException("Instrument Series is not invalid ")
                else:
                    self._get_sample_cmd = self.THSPH_COMMANDS[val][Command.GET_SAMPLE]

            log.debug('key = (%s), value = (%s)' % (key, val))

            self._param_dict.set_value(key, val)

    # noinspection PyUnusedLocal, PyMethodMayBeStatic
    def _handler_command_start_autosample(self, *args, **kwargs):
        """
        Switch into autosample mode.
        @retval next_state, (next_state, result)
        @throws InstrumentTimeoutException if device cannot be woken for command.
        @throws InstrumentProtocolException if command could not be built or misunderstood.
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    # noinspection PyMethodMayBeStatic
    def _handler_command_start_direct(self):
        """
        Start direct access
        """
        next_state = ProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    #######################################################################
    # Autosample State handlers.
    ########################################################################
    # noinspection PyUnusedLocal
    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.
        @retval next_state, (next_state, result)
        """
        next_state = None
        result = []

        self._init_params()

        self._setup_autosample_config()

        # Schedule auto sample task
        self._add_scheduler_event(ScheduledJob.AUTO_SAMPLE, ProtocolEvent.SCHEDULE_ACQUIRE_SAMPLE)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

        return next_state, (next_state, result)

    def _setup_autosample_config(self):
        """
        Set up auto sample configuration and add it to the scheduler.
        """
        # Start the scheduler to poll the instrument for
        # data every sample interval seconds

        job_name = ScheduledJob.AUTO_SAMPLE
        polled_interval = self._param_dict.get(Parameter.INTERVAL)
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: polled_interval
                    }
                }
            }
        }
        self.set_init_params(config)

        # Start the scheduler if it is not running
        if not self._scheduler:
            self.initialize_scheduler()

    # noinspection PyUnusedLocal, PyMethodMayBeStatic
    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit auto sample state. Remove the auto sample task
        """
        next_state = None
        result = []

        return next_state, (next_state, result)

    # noinspection PyUnusedLocal
    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Remove the auto sample task. Exit Auto sample state
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Stop the Auto Poll scheduling
        self._remove_scheduler(ScheduledJob.AUTO_SAMPLE)

        return next_state, (next_state, result)

    ########################################################################
    # Direct access handlers.
    ########################################################################

    # noinspection PyUnusedLocal
    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access 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):
        """
        Execute direct command
        """
        next_state = None
        result = []

        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return next_state, (next_state, result)

    # noinspection PyMethodMayBeStatic
    def _handler_direct_access_stop_direct(self):
        next_state = ProtocolState.COMMAND
        result = []

        return next_state, (next_state, result)

    def _build_simple_command(self, cmd, *args):
        """
        Build handler for basic THSPH commands.
        @param cmd the simple ooicore command to format.
        @retval The command to be sent to the device.
        """
        instrument_series = self._param_dict.get(Parameter.INSTRUMENT_SERIES)

        if cmd == Command.GET_SAMPLE:
            instrument_cmd = self.THSPH_COMMANDS[instrument_series][Command.GET_SAMPLE]
        else:
            raise InstrumentException('Unknown THSPH driver command  %s' % cmd)

        return "%s%s" % (instrument_cmd, NEWLINE)

    def _wakeup(self, wakeup_timeout=0, response_timeout=0):
        """
        There is no wakeup for this instrument.  Do nothing.
        @param wakeup_timeout The timeout to wake the device.
        @param response_timeout The time to look for response to a wakeup attempt.
        """
        pass
Beispiel #8
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.ENTER,
                                       self._handler_unknown_enter)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.EXIT,
                                       self._handler_unknown_exit)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.DISCOVER,
                                       self._handler_unknown_discover)

        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ENTER,
                                       self._handler_command_enter)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.EXIT,
                                       self._handler_command_exit)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.START_DIRECT,
                                       self._handler_command_start_direct)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.CLOCK_SYNC,
                                       self._handler_sync_clock)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.GET, self._handler_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.SET,
                                       self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS,
                                       ProtocolEvent.ENTER,
                                       self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS,
                                       ProtocolEvent.EXIT,
                                       self._handler_direct_access_exit)
        self._protocol_fsm.add_handler(
            ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT,
            self._handler_direct_access_execute_direct)
        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS,
                                       ProtocolEvent.STOP_DIRECT,
                                       self._handler_direct_access_stop_direct)

        # Add build handlers for device commands.
        self._add_build_handler(Command.BATTERY, self._build_simple_command)

        # Add response handlers for device commands.
        self._add_response_handler(Command.BATTERY,
                                   self._parse_battery_response)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        self._chunker = StringChunker(Protocol.sieve_function)

        self._add_scheduler_event(ScheduledJob.CLOCK_SYNC,
                                  ProtocolEvent.CLOCK_SYNC)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples and status
        """
        matchers = []
        return_list = []

        matchers.append(RASFL_SampleDataParticle.regex_compiled())

        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))
        """
        if return_list != []:
            log.debug("sieve_function: raw_data=%s, return_list=%s" %(raw_data, return_list))
        """
        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        log.debug("_got_chunk: chunk=%s" % chunk)
        self._extract_sample(RASFL_SampleDataParticle,
                             RASFL_SampleDataParticle.regex_compiled(), chunk,
                             timestamp)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # implement virtual methods from base class.
    ########################################################################

    def apply_startup_params(self):
        """
        Apply sample_interval startup parameter.  
        """

        config = self.get_startup_config()
        log.debug("apply_startup_params: startup config = %s" % config)
        if config.has_key(Parameter.SAMPLE_INTERVAL):
            log.debug("apply_startup_params: setting sample_interval to %d" %
                      config[Parameter.SAMPLE_INTERVAL])
            self._param_dict.set_value(Parameter.SAMPLE_INTERVAL,
                                       config[Parameter.SAMPLE_INTERVAL])

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_enter(self, *args, **kwargs):
        """
        Enter unknown state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_unknown_exit(self, *args, **kwargs):
        """
        Exit unknown state.
        """
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode).
        @retval (next_state, result), (ProtocolState.COMMAND, None) if successful.
        """

        # force to command mode, this instrument has no autosample mode
        next_state = ProtocolState.COMMAND
        result = ResourceAgentState.COMMAND

        log.debug("_handler_unknown_discover: state = %s", next_state)
        return (next_state, result)

    ########################################################################
    # Command handlers.
    # just implemented to make DA possible, instrument has no actual command mode
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """

        # Command device to update parameters and send a config change event if needed.
        self._update_params()

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        """
        Exit command state.
        """
        pass

    def _handler_command_set(self, *args, **kwargs):
        """
        no writable parameters so does nothing, just implemented to make framework happy
        """

        next_state = None
        result = None
        return (next_state, result)

    def _handler_command_start_direct(self, *args, **kwargs):
        """
        """
        result = None
        next_state = ProtocolState.DIRECT_ACCESS
        next_agent_state = ResourceAgentState.DIRECT_ACCESS

        return (next_state, (next_agent_state, result))

    ########################################################################
    # Direct access handlers.
    ########################################################################

    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

        self._sent_cmds = []

    def _handler_direct_access_exit(self, *args, **kwargs):
        """
        Exit direct access state.
        """
        pass

    def _handler_direct_access_execute_direct(self, data):
        next_state = None
        result = None

        self._do_cmd_direct(data)

        return (next_state, result)

    def _handler_direct_access_stop_direct(self):
        result = None
        next_state = ProtocolState.COMMAND
        next_agent_state = ResourceAgentState.COMMAND

        return (next_state, (next_agent_state, result))

    ########################################################################
    # general handlers.
    ########################################################################

    def _handler_sync_clock(self, *args, **kwargs):
        """
        sync clock close to a second edge 
        @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)).
        @throws InstrumentTimeoutException if device respond correctly.
        @throws InstrumentProtocolException if command could not be built or misunderstood.
        """

        next_state = None
        next_agent_state = None
        result = None

        time_format = "%Y/%m/%d %H:%M:%S"
        str_val = get_timestamp_delayed(time_format)
        log.debug("Setting instrument clock to '%s'", str_val)
        self._do_cmd_resp(Command.STOP, expected_prompt=Prompt.STOPPED)
        try:
            self._do_cmd_resp(Command.SET_CLOCK,
                              str_val,
                              expected_prompt=Prompt.CR_NL)
        finally:
            # ensure that we try to start the instrument sampling again
            self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO)

        return (next_state, (next_agent_state, result))

    ########################################################################
    # Private helpers.
    ########################################################################

    def _wakeup(self, wakeup_timeout=10, response_timeout=3):
        """
        Over-ridden because waking this instrument up is a multi-step process with
        two different requests required
        @param wakeup_timeout The timeout to wake the device.
        @param response_timeout The time to look for response to a wakeup attempt.
        @throw InstrumentTimeoutException if the device could not be woken.
        """
        sleep_time = .1
        command = Command.END_OF_LINE

        # Grab start time for overall wakeup timeout.
        starttime = time.time()

        while True:
            # Clear the prompt buffer.
            log.debug("_wakeup: clearing promptbuf: %s" % self._promptbuf)
            self._promptbuf = ''

            # Send a command and wait delay amount for response.
            log.debug('_wakeup: Sending command %s, delay=%s' %
                      (command.encode("hex"), response_timeout))
            for char in command:
                self._connection.send(char)
                time.sleep(INTER_CHARACTER_DELAY)
            sleep_amount = 0
            while True:
                time.sleep(sleep_time)
                if self._promptbuf.find(Prompt.COMMAND_INPUT) != -1:
                    # instrument is awake
                    log.debug('_wakeup: got command input prompt %s' %
                              Prompt.COMMAND_INPUT)
                    # add inter-character delay which _do_cmd_resp() incorrectly doesn't add to the start of a transmission
                    time.sleep(INTER_CHARACTER_DELAY)
                    return Prompt.COMMAND_INPUT
                if self._promptbuf.find(Prompt.ENTER_CTRL_C) != -1:
                    command = Command.CONTROL_C
                    break
                if self._promptbuf.find(Prompt.PERIOD) == 0:
                    command = Command.CONTROL_C
                    break
                sleep_amount += sleep_time
                if sleep_amount >= response_timeout:
                    log.debug(
                        "_wakeup: expected response not received, buffer=%s" %
                        self._promptbuf)
                    break

            if time.time() > starttime + wakeup_timeout:
                raise InstrumentTimeoutException(
                    "_wakeup(): instrument failed to wakeup in %d seconds time"
                    % wakeup_timeout)

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """
        self._cmd_dict.add(Capability.CLOCK_SYNC,
                           display_name="synchronize clock")

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with XR-420 parameters.
        For each parameter key add value formatting function for set commands.
        """
        # The parameter dictionary.
        self._param_dict = ProtocolParameterDict()

        # Add parameter handlers to parameter dictionary for instrument configuration parameters.
        self._param_dict.add(Parameter.BATTERY,
                             r'Battery: (.*)V',
                             lambda match: match.group(1),
                             lambda string: str(string),
                             type=ParameterDictType.STRING,
                             display_name="battery",
                             expiration=0,
                             visibility=ParameterDictVisibility.READ_ONLY)

        self._param_dict.add(
            Parameter.SAMPLE_INTERVAL,
            r'Not used. This parameter is not parsed from instrument response',
            None,
            self._int_to_string,
            type=ParameterDictType.INT,
            default_value=30,
            value=30,
            startup_param=True,
            display_name="sample_interval",
            visibility=ParameterDictVisibility.IMMUTABLE)

    def _do_cmd_resp(self, cmd, *args, **kwargs):
        CommandResponseInstrumentProtocol._do_cmd_resp(
            self, cmd, args, kwargs, write_delay=INTER_CHARACTER_DELAY)

    def _update_params(self, *args, **kwargs):
        """
        Update the parameter dictionary. 
        """

        log.debug("_update_params:")
        self._do_cmd_resp(Command.BATTERY)

    def _parse_battery_response(self, response, prompt):
        """
        Parse handler for battery command.
        @param response command response string.
        @param prompt prompt following command response.        
        @throws InstrumentProtocolException if clock command misunderstood.
        """
        log.debug("_parse_battery_response: response=%s, prompt=%s" %
                  (response, prompt))
        if prompt == Prompt.UNRECOGNIZED_COMMAND:
            raise InstrumentProtocolException(
                'battery command not recognized: %s.' % response)

        if not self._param_dict.update(response):
            raise InstrumentProtocolException(
                'battery command not parsed: %s.' % response)

        return
Beispiel #9
0
class Protocol(InstrumentProtocol):
    def __init__(self, driver_event):
        super(Protocol, self).__init__(driver_event)
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        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.GET, self._handler_get),
                (ProtocolEvent.SET, self._handler_set),
                (ProtocolEvent.START_AUTOSAMPLE,
                 self._handler_command_start_autosample),
            ),
            ProtocolState.AUTOSAMPLE: (
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_autosample_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.FLUSH, self._flush),
                (ProtocolEvent.STOP_AUTOSAMPLE,
                 self._handler_autosample_stop_autosample),
            ),
            ProtocolState.STOPPING: (
                (ProtocolEvent.ENTER, self._handler_stopping_enter),
                (ProtocolEvent.EXIT, self._handler_stopping_exit),
                (ProtocolEvent.FLUSH, self._flush),
            ),
            ProtocolState.WRITE_ERROR: (
                (ProtocolEvent.ENTER, self._handler_write_error_enter),
                (ProtocolEvent.EXIT, self._handler_write_error_exit),
                (ProtocolEvent.CLEAR_WRITE_ERROR,
                 self._handler_clear_write_error),
            )
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Build dictionaries for driver schema
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)
        self._logs = {}
        self._filled_logs = []
        self._pickle_cache = []

        # persistent store, cannot initialize until startup config has been applied
        # since we need the address for postgres
        self._persistent_store = None

        # lock for flush actions to prevent writing or altering the data files
        # during flush
        self._lock = Lock()
        self._pktid = 0

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE,
                           display_name="Start Autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE,
                           display_name="Stop Autosample")
        self._cmd_dict.add(Capability.GET, display_name="Get")
        self._cmd_dict.add(Capability.SET, display_name="Set")
        self._cmd_dict.add(Capability.DISCOVER, display_name="Discover")
        self._cmd_dict.add(Capability.CLEAR_WRITE_ERROR,
                           display_name="Clear Write Error")

    def _build_param_dict(self):
        self._param_dict.add(
            Parameter.REFDES,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name='Reference Designator',
            description='Reference Designator for this driver',
            type=ParameterDictType.STRING)
        self._param_dict.add(
            Parameter.SOURCE_REGEX,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name='Source Filter Regex',
            description='Filter sources to be processed from the ORB',
            type=ParameterDictType.STRING,
            value_description='Regular expression')
        self._param_dict.add(
            Parameter.FLUSH_INTERVAL,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name='Flush Interval',
            description='Interval after which all records are flushed to disk',
            type=ParameterDictType.INT,
            value_description='Interval, in seconds',
            units=Units.SECOND)
        self._param_dict.add(
            Parameter.DB_ADDR,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value='localhost',
            display_name='Database Address',
            description='Postgres database IP address or hostname',
            type=ParameterDictType.STRING,
            value_description='IP address or hostname')
        self._param_dict.add(
            Parameter.DB_PORT,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value=5432,
            display_name='Database Port',
            description='Postgres database port number',
            type=ParameterDictType.INT,
            value_description='Integer port number (default 5432)')
        self._param_dict.add(
            Parameter.FILE_LOCATION,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value="./antelope_data",
            display_name='File Location',
            description='Root file path of the packet data files',
            type=ParameterDictType.STRING,
            value_description=
            'String representing the packet data root file path')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_persistent_dict(self):
        name = 'antelope'
        refdes = self._param_dict.get(Parameter.REFDES)
        host = self._param_dict.get(Parameter.DB_ADDR)
        port = self._param_dict.get(Parameter.DB_PORT)

        self._persistent_store = PersistentStoreDict(name,
                                                     refdes,
                                                     host=host,
                                                     port=port)
        if 'pktid' not in self._persistent_store:
            self._persistent_store['pktid'] = ORBOLDEST

    def _handler_set(self, *args, **kwargs):
        pass

    def _update_params(self, *args, **kwargs):
        pass

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        self._verify_not_readonly(*args, **kwargs)

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Set the base directory for the packet data file location.
        PacketLog.base_dir = self._param_dict.get(Parameter.FILE_LOCATION)

    def _flush(self):
        log.info('flush')
        particles = []
        with self._lock:
            log.info('got lock')

            # On the last flush, close all the bins.
            last_flush = self.get_current_state() == ProtocolState.STOPPING
            if last_flush:
                self._filled_logs.extend(self._logs.values())
                self._logs = {}

            for _log in self._logs.itervalues():
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (
                        ProtocolState.WRITE_ERROR, None)

                particles.append(
                    AntelopeMetadataParticle(
                        _log,
                        preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP)
                )

            for _log in self._filled_logs:
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (
                        ProtocolState.WRITE_ERROR, None)

                particles.append(
                    AntelopeMetadataParticle(
                        _log,
                        preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP)
                )
                _log.data = []

            self._filled_logs = []
            log.info('updating persistent store')
            self._persistent_store['pktid'] = self._pktid

        for particle in particles:
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())

        if last_flush:
            self.stop_scheduled_job(ScheduledJob.FLUSH)
            return ProtocolState.COMMAND, (ProtocolState.COMMAND, None)

        return None, (None, None)

    # noinspection PyProtectedMember
    def _orbstart(self):
        self._connection._command_port_agent(
            'orbselect %s' % self._param_dict.get(Parameter.SOURCE_REGEX))
        self._connection._command_port_agent('orbseek %s' %
                                             self._persistent_store['pktid'])
        self._connection._command_port_agent('orbstart')

    # noinspection PyProtectedMember
    def _orbstop(self):
        self._connection._command_port_agent('orbstop')

    def stop_scheduled_job(self, schedule_job):
        """
        Remove the scheduled job
        @param schedule_job scheduling job.
        """
        if self._scheduler is not None:
            try:
                self._remove_scheduler(schedule_job)
            except KeyError:
                log.warn("_remove_scheduler could not find %s", schedule_job)

    def start_scheduled_job(self, param, schedule_job, protocol_event):
        """
        Add a scheduled job
        """
        self.stop_scheduled_job(schedule_job)
        val = self._param_dict.get(param)

        try:
            seconds = int(val)
        except ValueError:
            raise InstrumentParameterException(
                'Bad interval. Cannot parse %r as integer' % val)

        if seconds > 0:
            config = {
                DriverConfigKey.SCHEDULER: {
                    schedule_job: {
                        DriverSchedulerConfigKey.TRIGGER: {
                            DriverSchedulerConfigKey.TRIGGER_TYPE:
                            TriggerType.INTERVAL,
                            DriverSchedulerConfigKey.SECONDS: seconds
                        }
                    }
                }
            }
            self.set_init_params(config)
            self._add_scheduler_event(schedule_job, protocol_event)

    def got_data(self, port_agent_packet):
        data_length = port_agent_packet.get_data_length()
        data_type = port_agent_packet.get_header_type()

        if data_type == PortAgentPacket.PICKLED_FROM_INSTRUMENT:
            self._pickle_cache.append(port_agent_packet.get_data())
            # this is the max size (65535) minus the header size (16)
            # any packet of this length will be followed by one or more packets
            # with additional data. Keep accumulating packets until we have
            # the complete data, then unpickle.
            if data_length != 65519:
                data = pickle.loads(''.join(self._pickle_cache))
                self._pickle_cache = []
                self._bin_data(data)
        else:
            raise InstrumentProtocolException(
                'Received unpickled data from port agent')

    def got_raw(self, port_agent_packet):
        pass

    def _get_bin(self, packet):
        rate_map = {
            1: 86400,  # 1 day
            8: 86400,  # 1 day
            40: 86400,  # 1 day
            200: 86400,  # 1 day
            64000: 60 * 5,  # 5 minutes
            256000: 60,  # 1 minute
        }
        start_time = packet['time']
        rate = packet['samprate']
        bin_size = rate_map.get(rate, 60)
        bin_value = int(start_time / bin_size)
        bin_start = bin_value * bin_size
        bin_end = (bin_value + 1) * bin_size

        return bin_start, bin_end

    def _bin_data(self, packet):
        key = '%s.%s.%s.%s' % (packet['net'], packet.get(
            'location', ''), packet.get('sta', ''), packet['chan'])
        start, end = self._get_bin(packet)

        with self._lock:
            self._pktid = packet['pktid']

            if key not in self._logs:
                self._logs[key] = PacketLog.from_packet(
                    packet, end, self._param_dict.get(Parameter.REFDES))

            try:
                while True:
                    packet = self._logs[key].add_packet(packet)
                    if packet is None:
                        break
                    # residual, we need a new bin
                    # log is complete, move to holding list until next flush
                    self._filled_logs.append(self._logs[key])
                    del self._logs[key]
                    # create the new log...
                    start, end = self._get_bin(packet)
                    self._logs[key] = PacketLog.from_packet(
                        packet, end, self._param_dict.get(Parameter.REFDES))

            except GapException:
                # non-contiguous data detected, close this log and open a new one
                self._filled_logs.append(self._logs[key])
                del self._logs[key]
                # create the new log
                self._logs[key] = PacketLog.from_packet(
                    packet, end, self._param_dict.get(Parameter.REFDES))
                self._logs[key].add_packet(packet)

    ########################################################################
    # 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.
        """

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; always COMMAND.
        @return protocol_state, protocol_state
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # COMMAND handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        self._init_params()
        # We can't build the persistent dict until parameters are applied, so build it here
        if self._persistent_store is None:
            self._build_persistent_dict()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        """
        Exit command state.
        """

    def _handler_command_start_autosample(self, *args, **kwargs):
        """
        Switch into autosample mode.
        @return next_state, (next_state, result) if successful.
        """
        result = []

        # Ensure the current logs are clear to prevent residual data from being flushed.
        self._logs = {}
        self._filled_logs = []

        self._orbstart()
        next_state = ProtocolState.AUTOSAMPLE
        next_agent_state = ResourceAgentState.STREAMING

        return next_state, (next_state, result)

    ######################################################
    # AUTOSAMPLE handlers
    ######################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self.start_scheduled_job(Parameter.FLUSH_INTERVAL, ScheduledJob.FLUSH,
                                 ProtocolEvent.FLUSH)
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit autosample state.
        """
        self._orbstop()

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample and switch back to command mode.
        @return  next_state, (next_state, result) if successful.
        """
        self._orbstop()

        result = []
        next_state = ProtocolState.STOPPING
        next_agent_state = None

        return next_state, (next_state, result)

    ######################################################
    # STOPPING handlers
    ######################################################

    def _handler_stopping_enter(self, *args, **kwargs):
        """
        Enter stopping state.
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_stopping_exit(self, *args, **kwargs):
        """
        Exit stopping state.
        Stop the scheduled flush job and schedule flush one more time and
        indicate that it is the last flush before stopping auto sampling.
        """
        pass

    ######################################################
    # WRITE_ERROR handlers
    ######################################################

    def _handler_write_error_enter(self, *args, **kwargs):
        """
        Enter write error state.
        """
        self.stop_scheduled_job(ScheduledJob.FLUSH)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_write_error_exit(self, *args, **kwargs):
        """
        Exit write error state.
        """
        pass

    def _handler_clear_write_error(self, *args, **kwargs):
        """
        Clear the WRITE_ERROR state by transitioning to the COMMAND state.
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)
Beispiel #10
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    __metaclass__ = get_logging_metaclass(log_level='trace')

    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.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.START_AUTOSAMPLE, self._handler_command_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ACQUIRE_STATUS, self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.GET, self._handler_command_get)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add sample handlers.

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(self.sieve_function)

        log.info('processing particles with %d workers', POOL_SIZE)
        self._process_particles = True
        self._pending_particles = deque()
        self._processing_pool = multiprocessing.Pool(POOL_SIZE)

        self._particles_thread = Thread(target=self.particles_thread)
        self._particles_thread.setDaemon(True)
        self._particles_thread.start()

    def particles_thread(self):
        log.info('Starting particles generation thread.')
        processing_pool = self._processing_pool
        try:
            futures = {}

            while self._process_particles or futures:
                # Pull all processing requests from our request deque
                # Unless we have been instructed to terminate
                while True and self._process_particles:
                    try:
                        filepath, timestamp = self._pending_particles.popleft()
                        log.info('Received RAW file to process: %r %r', filepath, timestamp)
                        # Schedule for processing
                        # parse_datagram_file takes the filepath and returns a
                        # tuple containing the metadata and timestamp for creation
                        # of the particle
                        futures[(filepath, timestamp)] = processing_pool.apply_async(parse_particles_file, (filepath,))
                    except IndexError:
                        break

                # Grab our keys here, to avoid mutating the dictionary while iterating
                future_keys = sorted(futures)
                if future_keys:
                    log.debug('Awaiting completion of %d particles', len(future_keys))

                for key in future_keys:
                    future = futures[key]
                    if future.ready():
                        try:
                            # Job complete, remove the future from our dictionary and generate a particle
                            result = future.get()
                        except Exception as e:
                            result = e

                        futures.pop(key, None)

                        if isinstance(result, Exception):
                            self._driver_event(DriverAsyncEvent.ERROR, result)
                            continue

                        if result is not None:
                            metadata, internal_timestamp, data_times, power_data_dict, frequencies = result

                            filepath, timestamp = key
                            log.info('Completed particles with filepath: %r timestamp: %r', filepath, timestamp)

                            metadata_particle = ZplscBInstrumentDataParticle(metadata, port_timestamp=timestamp,
                                                                             internal_timestamp=internal_timestamp,
                                                                             preferred_timestamp=
                                                                             DataParticleKey.INTERNAL_TIMESTAMP)
                            parsed_sample = metadata_particle.generate()

                            if self._driver_event:
                                self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample)

                            for counter, data_timestamp in enumerate(data_times):
                                zp_data = {
                                    ZplscBParticleKey.FREQ_CHAN_1: frequencies[1],
                                    ZplscBParticleKey.VALS_CHAN_1: list(power_data_dict[1][counter]),
                                    ZplscBParticleKey.FREQ_CHAN_2: frequencies[2],
                                    ZplscBParticleKey.VALS_CHAN_2: list(power_data_dict[2][counter]),
                                    ZplscBParticleKey.FREQ_CHAN_3: frequencies[3],
                                    ZplscBParticleKey.VALS_CHAN_3: list(power_data_dict[3][counter]),
                                }

                                sample_particle = ZplscBSampleDataParticle(zp_data, port_timestamp=timestamp,
                                                                    internal_timestamp=data_timestamp,
                                                                    preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP)

                                parsed_sample_particles = sample_particle.generate()

                                if self._driver_event:
                                    self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample_particles)

                time.sleep(1)

        finally:
            if processing_pool:
                processing_pool.terminate()
                processing_pool.join()

    def shutdown(self):
        log.info('Shutting down ZPLSC protocol')
        super(Protocol, self).shutdown()
        # Do not add any more datagrams to the processing queue
        self._process_particles = False
        # Await completed processing of all datagrams for a maximum of 10 minutes
        log.info('Joining particles_thread')
        self._particles_thread.join(timeout=600)
        log.info('Completed ZPLSC protocol shutdown')

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        self._param_dict.add(
            Parameter.SCHEDULE,
            r'schedule:\s+(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="Schedule",
            description="Large block of text used to create the .yaml file defining the sampling schedule.",
            startup_param=True,
            default_value=yaml.dump(DEFAULT_CONFIG, default_flow_style=False))

        self._param_dict.add(
            Parameter.FTP_IP_ADDRESS,
            r'ftp address:\s+(\d\d\d\d\.\d\d\d\d\.\d\d\d\d\.\d\d\d)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP IP Address",
            description="IP address the driver uses to connect to the instrument FTP server.",
            startup_param=True,
            default_value=DEFAULT_HOST)

        self._param_dict.add(
            Parameter.FTP_USERNAME,
            r'username:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP User Name",
            description="Username used to connect to the FTP server.",
            startup_param=True,
            default_value=USER_NAME)

        self._param_dict.add(
            Parameter.FTP_PASSWORD,
            r'password:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Password",
            description="Password used to connect to the FTP server.",
            startup_param=True,
            default_value=PASSWORD)

        self._param_dict.add(
            Parameter.FTP_PORT,
            r'port:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Port",
            description="Location on the OOI infrastructure where .raw files stored.",
            startup_param=True,
            default_value=DEFAULT_PORT)

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_STATUS, display_name="Acquire Status")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # 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)

    @staticmethod
    def _handler_unknown_exit(*args, **kwargs):
        """
        Exit unknown state.
        """
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @retval next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Try to get the status to check if the instrument is alive
        host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
        port = self._param_dict.get_config_value(Parameter.FTP_PORT)
        response = self._url_request(host, port, '/status.json')

        if response is None:
            error_msg = "_handler_unknown_discover: Unable to connect to host: %s" % host
            log.error(error_msg)
            raise InstrumentConnectionException(error_msg)

        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        self._init_params()

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    @staticmethod
    def _handler_command_exit(*args, **kwargs):
        """
        Exit command state.
        """
        pass

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameters while in the command state.
        @param params List of the parameters to pass to the state
        @retval returns (next_state, result) where result is a dict {}. No
            agent state changes happening with Get, so no next_agent_state
        @throw InstrumentParameterException for invalid parameter
        """
        result_vals = {}

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException('_handler_command_get requires a parameter dict.')

        if Parameter.ALL in params:
            log.debug("Parameter ALL in params")
            params = Parameter.list()
            params.remove(Parameter.ALL)

        log.debug("_handler_command_get: params = %s", params)

        if params is None or not isinstance(params, list):
            raise InstrumentParameterException("GET parameter list not a list!")

        # fill the return values from the update
        for param in params:
            if not Parameter.has(param):
                raise InstrumentParameterException("Invalid parameter!")
            result_vals[param] = self._param_dict.get(param)
            self._param_dict.get_config_value(param)
        result = result_vals

        log.debug("Get finished, next_state: %s, result: %s", None, result)
        return None, result

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        @retval next state, result
        """
        startup = False

        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('_handler_command_set: command requires a parameter dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        # For each key, val in the params, set the param dictionary.
        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, None

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        # verify param is not readonly param
        self._verify_not_readonly(*args, **kwargs)

        for key, val in params.iteritems():
            log.debug("KEY = %s VALUE = %s", key, val)
            self._param_dict.set_value(key, val)
            if key == Parameter.SCHEDULE:
                self._ftp_schedule_file()

                # Load the schedule file
                host = self._param_dict.get(Parameter.FTP_IP_ADDRESS)
                port = self._param_dict.get_config_value(Parameter.FTP_PORT)
                log.debug("_set_params: stop the current schedule file")
                self._url_request(host, port, '/stop_schedule', data={})
                log.debug("_set_params: upload driver YAML file to host %s", host)
                res = self._url_request(host, port, '/load_schedule', data=json.dumps({'filename': YAML_FILE_NAME}))
                log.debug("_set_params: result from load = %s", res)

        log.debug("set complete, update params")

    def _ftp_schedule_file(self):
        """
        Construct a YAML schedule file and
        ftp the file to the Instrument server
        """
        # Create a temporary file and write the schedule YAML information to the file
        try:
            config_file = tempfile.TemporaryFile()
            log.debug("temporary file created")

            if config_file is None or not isinstance(config_file, file):
                raise InstrumentException("config_file is not a temp file!")

            config_file.write(self._param_dict.get(Parameter.SCHEDULE))
            config_file.seek(0)
            log.debug("finished writing config file:\n%r", self._param_dict.get(Parameter.SCHEDULE))

        except Exception as e:
            log.error("Create schedule YAML file exception: %s", e)
            raise e

        #  FTP the schedule file to the ZPLSC server
        host = ''

        try:
            log.debug("Create a ftp session")
            host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
            log.debug("Got host ip address %s", host)

            ftp_session = ftplib.FTP()
            ftp_session.connect(host)
            ftp_session.login(USER_NAME, PASSWORD)
            log.debug("ftp session was created...")

            ftp_session.set_pasv(False)
            ftp_session.cwd("config")

            ftp_session.storlines('STOR ' + YAML_FILE_NAME, config_file)
            files = ftp_session.dir()

            log.debug("*** Config yaml file sent: %s", files)

            ftp_session.quit()
            config_file.close()

        except (ftplib.socket.error, ftplib.socket.gaierror), e:
            log.error("ERROR: cannot reach FTP Host %s: %s ", host, e)
            raise InstrumentException("ERROR: cannot reach FTP Host %s " % host)

        log.debug("*** FTP %s to ftp host %s successfully", YAML_FILE_NAME, host)
Beispiel #11
0
class Protocol(InstrumentProtocol):
    __metaclass__ = META_LOGGER

    def __init__(self, driver_event):
        super(Protocol, self).__init__(driver_event)
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        handlers = {
            ProtocolState.UNKNOWN: (
                (ProtocolEvent.ENTER, self._handler_unknown_enter),
                (ProtocolEvent.EXIT, self._handler_unknown_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ),
            ProtocolState.AUTOSAMPLE: (
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_autosample_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.FLUSH, self._flush),
                (ProtocolEvent.CONFIG_ERROR, self._handler_config_error),
            ),
            ProtocolState.WRITE_ERROR: (
                (ProtocolEvent.ENTER, self._handler_error_enter),
                (ProtocolEvent.EXIT, self._handler_error_exit),
            ),
            ProtocolState.CONFIG_ERROR: (
                (ProtocolEvent.ENTER, self._handler_error_enter),
                (ProtocolEvent.EXIT, self._handler_error_exit),
            )
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Build dictionaries for driver schema
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)
        self._logs = {}
        self._filled_logs = []
        self._pickle_cache = []

        self._persistent_store = None

        # lock for flush actions to prevent writing or altering the data files
        # during flush
        self._lock = Lock()
        self._pktid = None

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.GET, display_name="Get")
        self._cmd_dict.add(Capability.SET, display_name="Set")
        self._cmd_dict.add(Capability.DISCOVER, display_name="Discover")

    def _build_param_dict(self):
        self._param_dict.add(
            Parameter.REFDES,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name='Reference Designator',
            description='Reference Designator for this driver',
            type=ParameterDictType.STRING)
        self._param_dict.add(
            Parameter.SOURCE_REGEX,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name='Source Filter Regex',
            description='Filter sources to be processed from the ORB',
            type=ParameterDictType.STRING,
            value_description='Regular expression')
        self._param_dict.add(
            Parameter.FILE_LOCATION,
            'NA',
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value="./antelope_data",
            display_name='File Location',
            description='Root file path of the packet data files',
            type=ParameterDictType.STRING,
            value_description=
            'String representing the packet data root file path')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_persistent_dict(self):
        refdes = self._param_dict.get(Parameter.REFDES)

        self._persistent_store = ConsulPersistentStore(refdes)
        if 'pktid' not in self._persistent_store:
            self._persistent_store['pktid'] = ORBOLDEST

    def _handler_set(self, *args, **kwargs):
        pass

    def _update_params(self, *args, **kwargs):
        pass

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        self._verify_not_readonly(*args, **kwargs)

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Set the base directory for the packet data file location.
        PacketLog.base_dir = self._param_dict.get(Parameter.FILE_LOCATION)

    def _flush(self):
        particles = []
        with self._lock:
            for _log in self._logs.itervalues():
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (
                        ProtocolState.WRITE_ERROR, None)

                particles.append(
                    AntelopeMetadataParticle(
                        _log,
                        preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP)
                )

            for _log in self._filled_logs:
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (
                        ProtocolState.WRITE_ERROR, None)

                particles.append(
                    AntelopeMetadataParticle(
                        _log,
                        preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP)
                )
                _log.data = []

            self._filled_logs = []
            if self._pktid is not None:
                log.info('updating persistent store')
                self._persistent_store['pktid'] = self._pktid

        for particle in particles:
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())

        return None, (None, None)

    # noinspection PyProtectedMember
    def _orbstart(self):
        self._connection._command_port_agent(
            'orbselect %s' % self._param_dict.get(Parameter.SOURCE_REGEX))
        self._connection._command_port_agent('orbseek %s' %
                                             self._persistent_store['pktid'])
        self._connection._command_port_agent('orbstart')

    # noinspection PyProtectedMember
    def _orbstop(self):
        self._connection._command_port_agent('orbstop')

    def stop_scheduled_job(self, schedule_job):
        """
        Remove the scheduled job
        @param schedule_job scheduling job.
        """
        if self._scheduler is not None:
            try:
                self._remove_scheduler(schedule_job)
            except KeyError:
                log.warn("_remove_scheduler could not find %s", schedule_job)

    def got_data(self, port_agent_packet):
        data_length = port_agent_packet.get_data_length()
        data_type = port_agent_packet.get_header_type()

        if data_type == PortAgentPacket.PICKLED_FROM_INSTRUMENT:
            self._pickle_cache.append(port_agent_packet.get_data())
            # this is the max size (65535) minus the header size (16)
            # any packet of this length will be followed by one or more packets
            # with additional data. Keep accumulating packets until we have
            # the complete data, then unpickle.
            if data_length != 65519:
                data = pickle.loads(''.join(self._pickle_cache))
                self._pickle_cache = []
                self._bin_data(data)
        else:
            raise InstrumentProtocolException(
                'Received unpickled data from port agent')

    def got_raw(self, port_agent_packet):
        pass

    def _get_bin(self, packet):
        rate_map = {
            1: 86400,  # 1 day
            8: 86400,  # 1 day
            40: 86400,  # 1 day
            200: 86400,  # 1 day
            64000: 60 * 5,  # 5 minutes
            256000: 60,  # 1 minute
        }
        start_time = packet['time']
        rate = packet['samprate']
        bin_size = rate_map.get(rate, 60)
        bin_value = int(start_time / bin_size)
        bin_start = bin_value * bin_size
        bin_end = (bin_value + 1) * bin_size

        return bin_start, bin_end

    def _bin_data(self, packet):
        key = '%s.%s.%s.%s' % (packet['net'], packet.get(
            'location', ''), packet.get('sta', ''), packet['chan'])
        start, end = self._get_bin(packet)

        with self._lock:
            self._pktid = packet['pktid']

            if key not in self._logs:
                self._logs[key] = PacketLog.from_packet(
                    packet, end, self._param_dict.get(Parameter.REFDES))

            try:
                while True:
                    packet = self._logs[key].add_packet(packet)
                    if packet is None:
                        break
                    # residual, we need a new bin
                    # log is complete, move to holding list until next flush
                    self._filled_logs.append(self._logs[key])
                    del self._logs[key]
                    # create the new log...
                    start, end = self._get_bin(packet)
                    self._logs[key] = PacketLog.from_packet(
                        packet, end, self._param_dict.get(Parameter.REFDES))

            except GapException:
                # non-contiguous data detected, close this log and open a new one
                self._filled_logs.append(self._logs[key])
                del self._logs[key]
                # create the new log
                self._logs[key] = PacketLog.from_packet(
                    packet, end, self._param_dict.get(Parameter.REFDES))
                self._logs[key].add_packet(packet)

    ########################################################################
    # 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)
        self._add_scheduler_event(ScheduledJob.DISCOVER,
                                  ProtocolEvent.DISCOVER)

    def _handler_unknown_exit(self, *args, **kwargs):
        """
        Exit unknown state.
        """
        self.stop_scheduled_job(ScheduledJob.DISCOVER)

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; always COMMAND.
        @return protocol_state, protocol_state
        """
        next_state = None
        if self._connection:
            next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ######################################################
    # AUTOSAMPLE handlers
    ######################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        try:
            self._init_params()
            self._build_persistent_dict()
            self._add_scheduler_event(ScheduledJob.FLUSH, ProtocolEvent.FLUSH)
            self._orbstart()

        except InstrumentProtocolException:
            self._async_raise_fsm_event(ProtocolEvent.CONFIG_ERROR)

        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit autosample state.
        """
        self.stop_scheduled_job(ScheduledJob.FLUSH)
        self._orbstop()

    def _handler_config_error(self, *args, **kwargs):
        next_state = ProtocolState.CONFIG_ERROR
        result = None
        return next_state, (next_state, result)

    ######################################################
    # ERROR handlers
    ######################################################

    def _handler_error_enter(self, *args, **kwargs):
        """
        Enter error state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_error_exit(self, *args, **kwargs):
        """
        Exit error state.
        This should never occur, this state is a dead end.
        """
        pass
Beispiel #12
0
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.
        """
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        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.AUTOSAMPLE,
                                       ProtocolEvent.ENTER,
                                       self._handler_autosample_enter)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.EXIT,
                                       self._handler_autosample_exit)

        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(Protocol.sieve_function)

        self._build_driver_dict()
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples and status
        :param raw_data: raw data from instrument
        """
        raw_data_len = len(raw_data)
        return_list = []

        # look for samples
        # OPTAA record looks like this:
        # ff00ff00  <- packet registration
        # 02d0      <- record length minus checksum
        # ...       <- data
        # 2244      <- checksum
        # 00        <- pad
        for match in PACKET_REGISTRATION_REGEX.finditer(raw_data):
            # make sure I have at least 6 bytes (packet registration plus 2 bytes for record length)
            start = match.start()
            if (start + 6) <= raw_data_len:
                packet_length = struct.unpack_from('>H', raw_data,
                                                   start + 4)[0]
                # make sure we have enough data to construct a whole packet
                if (start + packet_length +
                        SIZE_OF_CHECKSUM_PLUS_PAD) <= raw_data_len:
                    # validate the checksum, if valid add to the return list
                    checksum = struct.unpack_from('>H', raw_data,
                                                  start + packet_length)[0]
                    calulated_checksum = sum(
                        bytearray(
                            raw_data[start:start + packet_length])) & 0xffff
                    if checksum == calulated_checksum:
                        return_list.append(
                            (match.start(), match.start() + packet_length +
                             SIZE_OF_CHECKSUM_PLUS_PAD))

        # look for status
        for match in STATUS_REGEX.finditer(raw_data):
            return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        self._extract_sample(OptaaSampleDataParticle,
                             PACKET_REGISTRATION_REGEX, chunk, timestamp)
        self._extract_sample(OptaaStatusDataParticle, STATUS_REGEX, chunk,
                             timestamp)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    ########################################################################
    # Unknown handlers.
    ########################################################################
    def _handler_unknown_enter(self, *args, **kwargs):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_unknown_exit(self, *args, **kwargs):
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can only be AUTOSAMPLE (instrument has no actual command mode).
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################
    def _handler_autosample_enter(self, *args, **kwargs):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        pass
Beispiel #13
0
class THSPHProtocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    SERIES_A = 'A'
    SERIES_B = 'B'
    SERIES_C = 'C'
    GET_SAMPLE_SERIES_A = 'aH*'  # Gets data sample from ADC for series A
    GET_SAMPLE_SERIES_B = 'bH*'  # Gets data sample from ADC for series B
    GET_SAMPLE_SERIES_C = 'cH*'  # Gets data sample from ADC for series C

    # THSPH commands for instrument series A, B and C
    THSPH_COMMANDS = {
        SERIES_A: {
            Command.GET_SAMPLE: GET_SAMPLE_SERIES_A
        },
        SERIES_B: {
            Command.GET_SAMPLE: GET_SAMPLE_SERIES_B
        },
        SERIES_C: {
            Command.GET_SAMPLE: GET_SAMPLE_SERIES_C
        },
    }

    __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.
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.ENTER,
                                       self._handler_unknown_enter)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.EXIT,
                                       self._handler_unknown_exit)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN,
                                       ProtocolEvent.DISCOVER,
                                       self._handler_unknown_discover)

        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ENTER,
                                       self._handler_command_enter)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.EXIT,
                                       self._handler_command_exit)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.START_AUTOSAMPLE,
                                       self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ACQUIRE_SAMPLE,
                                       self._handler_command_acquire_sample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.GET,
                                       self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.SET,
                                       self._handler_command_set)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.START_DIRECT,
                                       self._handler_command_start_direct)

        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.SCHEDULE_ACQUIRE_SAMPLE,
                                       self._handler_command_acquire_sample)
        self._protocol_fsm.add_handler(
            ProtocolState.AUTOSAMPLE, ProtocolEvent.STOP_AUTOSAMPLE,
            self._handler_autosample_stop_autosample)

        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)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add build handlers for device commands.
        self._add_build_handler(Command.GET_SAMPLE, self._build_simple_command)

        # State state machine in COMMAND state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(THSPHProtocol.sieve_function)

        # Set Get Sample Command and Communication Test Command for Series A as default
        self._get_sample_cmd = self.GET_SAMPLE_SERIES_A

        self._direct_commands['Newline'] = self._newline
        self._direct_commands['Test A'] = 'aP*' + self._newline
        self._direct_commands['Test B'] = 'bP*' + self._newline
        self._direct_commands['Test C'] = 'cP*' + self._newline
        self._direct_commands[
            'Sample A'] = self.GET_SAMPLE_SERIES_A + self._newline
        self._direct_commands[
            'Sample B'] = self.GET_SAMPLE_SERIES_B + self._newline
        self._direct_commands[
            'Sample C'] = self.GET_SAMPLE_SERIES_C + self._newline

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        """
        matchers = []
        return_list = []

        matchers.append(THSPHParticle.regex_compiled())

        for matcher in matchers:
            log.trace('matcher: %r raw_data: %r', matcher.pattern, raw_data)
            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.
        """
        if not self._extract_sample(THSPHParticle,
                                    THSPHParticle.regex_compiled(), chunk,
                                    timestamp):
            raise InstrumentProtocolException("Unhandled chunk")

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_SAMPLE,
                           display_name="Acquire Sample")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with THSPH parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        # Add parameter handlers to parameter dict.
        self._param_dict.add(
            Parameter.INTERVAL,
            r'Auto Polled Interval = (\d+)',
            lambda match: int(match.group(1)),
            str,
            type=ParameterDictType.INT,
            units=Units.SECOND,
            display_name="Polled Interval",
            range=(1, 600),
            description="Polling interval, internal to driver (1-600).",
            visibility=ParameterDictVisibility.READ_WRITE,
            startup_param=True,
            direct_access=False,
            default_value=5)

        self._param_dict.add(
            Parameter.INSTRUMENT_SERIES,
            r'Instrument Series = ([A-C])',
            lambda match: int(match.group(1)),
            str,
            type=ParameterDictType.STRING,
            display_name="Instrument Series",
            range={
                'A': 'A',
                'B': 'B',
                'C': 'C'
            },
            description='Defines instance of instrument series [A, B, C].',
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            direct_access=False,
            default_value='A')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # Unknown State handlers.
    ########################################################################
    def _handler_unknown_enter(self, *args, **kwargs):

        # 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; Change next state to be COMMAND state.
        """
        next_state = ProtocolState.COMMAND
        result = []

        return next_state, (next_state, result)

    ########################################################################
    # Command State handlers.
    ########################################################################
    def _handler_command_acquire_sample(self, *args, **kwargs):
        """
        Get device status
        """
        timeout = time.time() + TIMEOUT

        next_state = None

        self._do_cmd_no_resp(Command.GET_SAMPLE, timeout=TIMEOUT)

        particles = self.wait_for_particles([DataParticleType.THSPH_PARSED],
                                            timeout)

        return next_state, (next_state, particles)

    def _handler_command_enter(self, *args, **kwargs):
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        pass

    def _handler_command_get(self, *args, **kwargs):
        """
        Get device parameters from the parameter dict.  First we set a baseline timestamp
        that all data expirations will be calculated against.  Then we try to get parameter
        value.  If we catch an expired parameter then we will update all parameters and get
        values using the original baseline time that we set at the beginning of this method.
        Assuming our _update_params is updating all parameter values properly then we can
        ensure that all data will be fresh.  Nobody likes stale data!
        @param args[0] list of parameters to retrieve, or DriverParameter.ALL.
        """
        next_state, result = self._handler_get(*args, **kwargs)
        # TODO - update return signature to match other handlers - next_state, (next_state, result)
        return next_state, result

    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @retval (next_state, result) tuple, (None, None).
        @throws InstrumentParameterException if missing set parameters, if set parameters not ALL and
        not a dict, or if parameter can't be properly formatted.
        """
        next_state = None
        result = []
        startup = False

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return next_state, result

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters internally to the driver. No issuing commands to the
        instrument needed for this driver.
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        # list can be null, like in the case of direct access params, in this case do nothing
        if not params:
            return

        # Do a range check before we start all sets
        for (key, val) in params.iteritems():

            if key == Parameter.INTERVAL and not (0 < val < 601):
                log.debug("Auto Sample Interval not in 1 to 600 range ")
                raise InstrumentParameterException(
                    "sample interval out of range [1, 600]")

            if key == Parameter.INSTRUMENT_SERIES:
                if val not in 'ABC':
                    log.debug("Instrument Series is not A, B or C ")
                    raise InstrumentParameterException(
                        "Instrument Series is not invalid ")
                else:
                    self._get_sample_cmd = self.THSPH_COMMANDS[val][
                        Command.GET_SAMPLE]

            log.debug('key = (%s), value = (%s)' % (key, val))

            self._param_dict.set_value(key, val)

    def _handler_command_start_autosample(self, *args, **kwargs):
        """
        Switch into autosample mode.
        @retval next_state, (next_state, result)
        @throws InstrumentTimeoutException if device cannot be woken for command.
        @throws InstrumentProtocolException if command could not be built or misunderstood.
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    def _handler_command_start_direct(self):
        """
        Start direct access
        """
        next_state = ProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    #######################################################################
    # Autosample State 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.
        @retval next_state, (next_state, result)
        """
        next_state = None
        result = []

        self._init_params()

        self._setup_autosample_config()

        # Schedule auto sample task
        self._add_scheduler_event(ScheduledJob.AUTO_SAMPLE,
                                  ProtocolEvent.SCHEDULE_ACQUIRE_SAMPLE)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

        return next_state, (next_state, result)

    def _setup_autosample_config(self):
        """
        Set up auto sample configuration and add it to the scheduler.
        """
        # Start the scheduler to poll the instrument for
        # data every sample interval seconds

        job_name = ScheduledJob.AUTO_SAMPLE
        polled_interval = self._param_dict.get(Parameter.INTERVAL)
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE:
                        TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: polled_interval
                    }
                }
            }
        }
        self.set_init_params(config)

        # Start the scheduler if it is not running
        if not self._scheduler:
            self.initialize_scheduler()

    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit auto sample state. Remove the auto sample task
        """
        next_state = None
        result = []

        return next_state, (next_state, result)

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Remove the auto sample task. Exit Auto sample state
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Stop the Auto Poll scheduling
        self._remove_scheduler(ScheduledJob.AUTO_SAMPLE)

        return next_state, (next_state, result)

    ########################################################################
    # Direct access handlers.
    ########################################################################

    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access 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):
        """
        Execute direct command
        """
        next_state = None
        result = []

        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return next_state, (next_state, result)

    def _handler_direct_access_stop_direct(self):
        next_state = ProtocolState.COMMAND
        result = []

        return next_state, (next_state, result)

    def _build_simple_command(self, cmd, *args):
        """
        Build handler for basic THSPH commands.
        @param cmd the simple ooicore command to format.
        @retval The command to be sent to the device.
        """
        instrument_series = self._param_dict.get(Parameter.INSTRUMENT_SERIES)

        if cmd == Command.GET_SAMPLE:
            instrument_cmd = self.THSPH_COMMANDS[instrument_series][
                Command.GET_SAMPLE]
        else:
            raise InstrumentException('Unknown THSPH driver command  %s' % cmd)

        return "%s%s" % (instrument_cmd, NEWLINE)

    def _wakeup(self, wakeup_timeout=0, response_timeout=0):
        """
        There is no wakeup for this instrument.  Do nothing.
        @param wakeup_timeout The timeout to wake the device.
        @param response_timeout The time to look for response to a wakeup attempt.
        """
        pass
Beispiel #14
0
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.
        """
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        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.AUTOSAMPLE, ProtocolEvent.ENTER, self._handler_autosample_enter)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE, ProtocolEvent.EXIT, self._handler_autosample_exit)

        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(Protocol.sieve_function)

        self._build_driver_dict()
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples and status
        :param raw_data: raw data from instrument
        """
        raw_data_len = len(raw_data)
        return_list = []

        # look for samples
        # OPTAA record looks like this:
        # ff00ff00  <- packet registration
        # 02d0      <- record length minus checksum
        # ...       <- data
        # 2244      <- checksum
        # 00        <- pad
        for match in PACKET_REGISTRATION_REGEX.finditer(raw_data):
            # make sure I have at least 6 bytes (packet registration plus 2 bytes for record length)
            start = match.start()
            if (start + 6) <= raw_data_len:
                packet_length = struct.unpack_from('>H', raw_data, start + 4)[0]
                # make sure we have enough data to construct a whole packet
                if (start + packet_length + SIZE_OF_CHECKSUM_PLUS_PAD) <= raw_data_len:
                    # validate the checksum, if valid add to the return list
                    checksum = struct.unpack_from('>H', raw_data, start + packet_length)[0]
                    calulated_checksum = sum(bytearray(raw_data[start:start + packet_length])) & 0xffff
                    if checksum == calulated_checksum:
                        return_list.append((match.start(), match.start() + packet_length + SIZE_OF_CHECKSUM_PLUS_PAD))

        # look for status
        for match in STATUS_REGEX.finditer(raw_data):
            return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        self._extract_sample(OptaaSampleDataParticle, PACKET_REGISTRATION_REGEX, chunk, timestamp)
        self._extract_sample(OptaaStatusDataParticle, STATUS_REGEX, chunk, timestamp)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    ########################################################################
    # Unknown handlers.
    ########################################################################
    def _handler_unknown_enter(self, *args, **kwargs):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_unknown_exit(self, *args, **kwargs):
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can only be AUTOSAMPLE (instrument has no actual command mode).
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################
    def _handler_autosample_enter(self, *args, **kwargs):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        pass
Beispiel #15
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample),
                (ProtocolEvent.START_LEVELING, self._handler_start_leveling),
                (ProtocolEvent.STOP_LEVELING, self._handler_stop_leveling),
                (ProtocolEvent.NANO_TIME_SYNC, self._handler_time_sync),
                (ProtocolEvent.START_HEATER, self._handler_start_heater),
                (ProtocolEvent.STOP_HEATER, self._handler_stop_heater),
                (ProtocolEvent.LEVELING_TIMEOUT, self._handler_leveling_timeout),
                (ProtocolEvent.HEATER_TIMEOUT, self._handler_heater_timeout),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample),
                (ProtocolEvent.START_LEVELING, self._handler_start_leveling),
                (ProtocolEvent.STOP_LEVELING, self._handler_stop_leveling),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.NANO_TIME_SYNC, self._handler_time_sync),
                (ProtocolEvent.START_HEATER, self._handler_start_heater),
                (ProtocolEvent.STOP_HEATER, self._handler_stop_heater),
                (ProtocolEvent.LEVELING_TIMEOUT, self._handler_leveling_timeout),
                (ProtocolEvent.HEATER_TIMEOUT, self._handler_heater_timeout),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_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)

        # Construct the metadata dictionaries
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # Add build handlers for device commands.
        for command in InstrumentCommand.list():
            if command in [InstrumentCommand.NANO_SET_RATE, InstrumentCommand.HEAT]:
                self._add_build_handler(command, self._build_command_with_value)
            else:
                self._add_build_handler(command, self._build_simple_command)

        # # Add response handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_response_handler(command, self._generic_response_handler)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        # create chunker
        self._chunker = StringChunker(Protocol.sieve_function)

        self._last_data_timestamp = 0
        self.has_pps = True

        # set up scheduled event handling
        self.initialize_scheduler()
        self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, ProtocolEvent.ACQUIRE_STATUS)
        self._add_scheduler_event(ScheduledJob.NANO_TIME_SYNC, ProtocolEvent.NANO_TIME_SYNC)

    @staticmethod
    def sieve_function(raw_data):
        """
        Sort data in the chunker...
        @param raw_data: Data to be searched for samples
        @return: list of (start,end) tuples
        """
        matchers = []
        return_list = []

        matchers.append(particles.HeatSampleParticle.regex_compiled())
        matchers.append(particles.IrisSampleParticle.regex_compiled())
        matchers.append(particles.NanoSampleParticle.regex_compiled())
        matchers.append(particles.LilySampleParticle.regex_compiled())
        matchers.append(particles.LilyLevelingParticle.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, ts):
        """
        Process chunk output by the chunker.  Generate samples and (possibly) react
        @param chunk: data
        @param ts: ntp timestamp
        @return sample
        @throws InstrumentProtocolException
        """
        possible_particles = [
            (particles.LilySampleParticle, self._check_for_autolevel),
            (particles.LilyLevelingParticle, self._check_completed_leveling),
            (particles.HeatSampleParticle, None),
            (particles.IrisSampleParticle, None),
            (particles.NanoSampleParticle, self._check_pps_sync),
        ]

        for particle_type, func in possible_particles:
            sample = self._extract_sample(particle_type, particle_type.regex_compiled(), chunk, ts)
            if sample:
                if func:
                    func(sample)
                return sample

        raise InstrumentProtocolException(u'unhandled chunk received by _got_chunk: [{0!r:s}]'.format(chunk))

    def _extract_sample(self, particle_class, regex, line, timestamp, publish=True):
        """
        Overridden to set the quality flag for LILY particles that are out of range.
        @param particle_class: Class type for particle
        @param regex: regular expression to verify data
        @param line: data
        @param timestamp: ntp timestamp
        @param publish: boolean to indicate if sample should be published
        @return: extracted sample
        """
        if regex.match(line):
            if particle_class == particles.LilySampleParticle and self._param_dict.get(Parameter.LEVELING_FAILED):
                particle = particle_class(line, port_timestamp=timestamp, quality_flag=DataParticleValue.OUT_OF_RANGE)
            else:
                particle = particle_class(line, port_timestamp=timestamp)
            parsed_sample = particle.generate()

            if publish and self._driver_event:
                self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample)

            return parsed_sample

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample")
        self._cmd_dict.add(Capability.ACQUIRE_STATUS, display_name="Acquire Status")
        self._cmd_dict.add(Capability.START_LEVELING, display_name="Start LILY Leveling")
        self._cmd_dict.add(Capability.STOP_LEVELING, display_name="Stop LILY Leveling")
        self._cmd_dict.add(Capability.START_HEATER, display_name="Start Heater")
        self._cmd_dict.add(Capability.STOP_HEATER, display_name="Stop Heater")

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        my_regex = 'Not used'
        ro, rw = ParameterDictVisibility.READ_ONLY, ParameterDictVisibility.READ_WRITE
        _bool, _float, _int = ParameterDictType.BOOL, ParameterDictType.FLOAT, ParameterDictType.INT

        parameters = {
            Parameter.AUTO_RELEVEL: {
                'type': _bool,
                'display_name': 'Automatic Releveling Enabled',
                'description': 'Enable LILY re-leveling automatically: (true | off)',
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.XTILT_TRIGGER: {
                'type': _float,
                'display_name': 'X-tilt Releveling Trigger',
                'description': 'The X-tilt value that must be exceeded before LILY auto releveling occurs.',
                'units': Prefixes.MICRO + Units.RADIAN,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.YTILT_TRIGGER: {
                'type': _float,
                'display_name': 'Y-tilt Releveling Trigger',
                'description': 'The Y-tilt value that must be exceeded before LILY auto releveling occurs.',
                'units': Prefixes.MICRO + Units.RADIAN,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.LEVELING_TIMEOUT: {
                'type': _int,
                'display_name': 'LILY Leveling Timeout',
                'description': 'Leveling timeout',
                'units': Units.SECOND,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.HEAT_DURATION: {
                'type': _int,
                'display_name': 'Heater Run Time Duration',
                'description': 'The number of hours the heater will run when it is given the command to turn on.',
                'units': Units.HOUR,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.OUTPUT_RATE: {
                'type': _int,
                'display_name': 'NANO Output Rate',
                'description': 'Sample rate',
                'units': Units.HERTZ,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.HEATER_ON: {
                'type': _bool,
                'display_name': 'Heater Running',
                'description': 'Indicates if the heater is running: (true | false)',
                'value': False,
                'visibility': ro,
            },
            Parameter.LILY_LEVELING: {
                'type': _bool,
                'display_name': 'Lily Leveling',
                'description': 'Indicates if LILY leveling is occurring: (true | false)',
                'value': False,
                'visibility': ro,
            },
            Parameter.LEVELING_FAILED: {
                'type': _bool,
                'display_name': 'LILY Leveling Failed',
                'description': 'Indicates if LILY leveling failed: (true | false)',
                'value': False,
                'visibility': ro,
            },
        }
        for param in parameters:
            self._param_dict.add(param, my_regex, None, None, **parameters[param])

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_command_with_value(self, cmd, value):
        """
        Build a simple command with one value specified
        @param cmd: instrument command
        @param value: value to be sent
        @return: command string
        """
        return '%s%d%s' % (cmd, value, NEWLINE)

    def _verify_set_values(self, params):
        """
        Verify supplied values are in range, if applicable
        @param params: Dictionary of Parameter:value pairs to be verified
        @throws InstrumentParameterException
        """
        constraints = ParameterConstraint.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params.iteritems():
            # verify this parameter exists
            if not Parameter.has(key):
                raise InstrumentParameterException('Received invalid parameter in SET: %s' % key)
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                constraint_string = 'Parameter: %s Value: %s Type: %s Minimum: %s Maximum: %s' % \
                                    (key, val, var_type, minimum, maximum)
                log.debug('SET CONSTRAINT: %s', constraint_string)
                # check bool values are actual booleans
                if var_type == bool:
                    if val not in [True, False]:
                        raise InstrumentParameterException('Non-boolean value!: %s' % constraint_string)
                # else, check if we can cast to the correct type
                else:
                    try:
                        var_type(val)
                    except ValueError:
                        raise InstrumentParameterException('Type mismatch: %s' % constraint_string)
                    # now, verify we are within min/max
                    if val < minimum or val > maximum:
                        raise InstrumentParameterException('Out of range: %s' % constraint_string)

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        self._verify_set_values(params)
        self._verify_not_readonly(*args, **kwargs)

        # if setting the output rate, get the current rate from the instrument first...
        if Parameter.OUTPUT_RATE in params:
            self._update_params()

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            log.debug('Config change: %r %r', old_config, new_config)
            if old_config[Parameter.OUTPUT_RATE] is not None:
                if int(old_config[Parameter.OUTPUT_RATE]) != int(new_config[Parameter.OUTPUT_RATE]):
                    self._do_cmd_no_resp(InstrumentCommand.NANO_SET_RATE, int(new_config[Parameter.OUTPUT_RATE]))
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _update_params(self, *args, **kwargs):
        """
        Update the param dictionary based on instrument response
        """
        result, _ = self._do_cmd_resp(InstrumentCommand.NANO_DUMP1,
                                      response_regex=particles.NanoStatusParticle.regex_compiled())
        rate = int(re.search(r'NANO,\*TH:(\d+)', result).group(1))
        self._param_dict.set_value(Parameter.OUTPUT_RATE, rate)

    def _wakeup(self, timeout, delay=1):
        """
        Overriding _wakeup; does not apply to this instrument
        """

    def add_to_buffer(self, data):
        """
        Overriding base class to reduce logging due to NANO high data rate
        @param data: data to be added to buffers
        """
        # Update the line and prompt buffers.
        self._linebuf += data
        self._promptbuf += data
        self._last_data_timestamp = time.time()

        # If our buffer exceeds the max allowable size then drop the leading
        # characters on the floor.
        max_size = self._max_buffer_size()
        if len(self._linebuf) > max_size:
            self._linebuf = self._linebuf[max_size * -1:]

        # If our buffer exceeds the max allowable size then drop the leading
        # characters on the floor.
        if len(self._promptbuf) > max_size:
            self._promptbuf = self._linebuf[max_size * -1:]

    def _max_buffer_size(self):
        """
        Overriding base class to increase max buffer size
        @return int max_buffer_size
        """
        return MAX_BUFFER_SIZE

    def _remove_leveling_timeout(self):
        """
        Clean up the leveling timer
        """
        try:
            self._remove_scheduler(ScheduledJob.LEVELING_TIMEOUT)
        except KeyError:
            log.debug('Unable to remove LEVELING_TIMEOUT scheduled job, job does not exist.')

    def _schedule_leveling_timeout(self):
        """
        Set up a leveling timer to make sure we don't stay in leveling state forever if something goes wrong
        """
        self._remove_leveling_timeout()
        dt = datetime.datetime.now() + datetime.timedelta(seconds=self._param_dict.get(Parameter.LEVELING_TIMEOUT))
        job_name = ScheduledJob.LEVELING_TIMEOUT
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.ABSOLUTE,
                        DriverSchedulerConfigKey.DATE: dt
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.LEVELING_TIMEOUT, ProtocolEvent.LEVELING_TIMEOUT)

    def _remove_heater_timeout(self):
        """
        Clean up the heater timer
        """
        try:
            self._remove_scheduler(ScheduledJob.HEATER_TIMEOUT)
        except KeyError:
            log.debug('Unable to remove HEATER_TIMEOUT scheduled job, job does not exist.')

    def _schedule_heater_timeout(self):
        """
        Set up a timer to set HEATER_ON to false around the time the heater shuts off
        """
        self._remove_heater_timeout()
        dt = datetime.datetime.now() + datetime.timedelta(hours=self._param_dict.get(Parameter.HEAT_DURATION))
        job_name = ScheduledJob.HEATER_TIMEOUT
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.ABSOLUTE,
                        DriverSchedulerConfigKey.DATE: dt
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.HEATER_TIMEOUT, ProtocolEvent.HEATER_TIMEOUT)

    def _stop_autosample(self):
        """
        Stop autosample, leveling if in progress.
        """
        self.leveling = False
        self._do_cmd_no_resp(InstrumentCommand.NANO_OFF)
        self._do_cmd_resp(InstrumentCommand.LILY_STOP_LEVELING, expected_prompt=Prompt.LILY_STOP_LEVELING)
        self._do_cmd_resp(InstrumentCommand.LILY_OFF, expected_prompt=Prompt.LILY_OFF)
        self._do_cmd_resp(InstrumentCommand.IRIS_OFF, expected_prompt=Prompt.IRIS_OFF)

    def _generic_response_handler(self, resp, prompt):
        """
        Pass through response handler
        @param resp: response
        @param prompt: prompt
        @return: (response, prompt)
        """
        return resp, prompt

    def _particle_to_dict(self, sample):
        """
        Convert a particle to a dictionary of value_id:value
        @param sample: particle to be parsed
        @return: dictionary representing the particle
        """
        sample_dict = {}
        values = sample.get(DataParticleKey.VALUES, [])
        for each in values:
            sample_dict[each[DataParticleKey.VALUE_ID]] = each[DataParticleKey.VALUE]
        return sample_dict

    def _check_for_autolevel(self, sample):
        """
        Check this sample, kick off a leveling event if out of range
        @param sample: sample to be checked
        """
        if self._param_dict.get(Parameter.AUTO_RELEVEL) and self.get_current_state() == ProtocolState.AUTOSAMPLE:
            # Find the current X and Y tilt values
            # If they exceed the trigger parameters, begin autolevel
            relevel = False
            sample = self._particle_to_dict(sample)
            x_tilt = abs(sample[particles.LilySampleParticleKey.X_TILT])
            y_tilt = abs(sample[particles.LilySampleParticleKey.Y_TILT])
            x_trig = int(self._param_dict.get(Parameter.XTILT_TRIGGER))
            y_trig = int(self._param_dict.get(Parameter.YTILT_TRIGGER))
            if x_tilt > x_trig or y_tilt > y_trig:
                self._async_raise_fsm_event(ProtocolEvent.START_LEVELING)

    def _failed_leveling(self, axis):
        """
        Handle a failed leveling event.  Set the failed flag, disable auto relevel and notify the operator
        @param axis: Axis which failed leveling
        """
        log.error('Detected leveling error in %s axis!', axis)
        # Read only parameter, must be set outside of handler
        self._param_dict.set_value(Parameter.LEVELING_FAILED, True)
        # Use the handler to disable auto relevel to raise a config change event if needed.
        self._handler_command_set({Parameter.AUTO_RELEVEL: False})
        raise InstrumentDataException('LILY Leveling (%s) Failed.  Disabling auto relevel' % axis)

    def _check_completed_leveling(self, sample):
        """
        Check this sample if leveling is complete or failed
        @param sample: Sample to be checked
        """
        sample = self._particle_to_dict(sample)
        status = sample[particles.LilyLevelingParticleKey.STATUS]
        if status is not None:
            # Leveling status update received
            # If leveling complete, send STOP_LEVELING, set the _leveling_failed flag to False
            if 'Leveled' in status:
                if self._param_dict.get(Parameter.LEVELING_FAILED):
                    self._handler_command_set({Parameter.LEVELING_FAILED: False})
                self._async_raise_fsm_event(ProtocolEvent.STOP_LEVELING)
            # Leveling X failed!  Set the flag and raise an exception to notify the operator
            # and disable auto leveling. Let the instrument attempt to level
            # in the Y axis.
            elif 'X Axis out of range' in status:
                self._failed_leveling('X')
            # Leveling X failed!  Set the flag and raise an exception to notify the operator
            # and disable auto leveling. Send STOP_LEVELING
            elif 'Y Axis out of range' in status:
                self._async_raise_fsm_event(ProtocolEvent.STOP_LEVELING)
                self._failed_leveling('Y')

    def _check_pps_sync(self, sample):
        """
        Check if PPS sync status has changed.  Update driver flag and, if appropriate, trigger a time sync
        @param sample: sample to be checked
        """
        sample = self._particle_to_dict(sample)
        pps_sync = sample[particles.NanoSampleParticleKey.PPS_SYNC] == 'P'
        if pps_sync:
            if not self.has_pps:
                # pps sync regained, sync the time
                self.has_pps = True
                if self.get_current_state() in [ProtocolState.COMMAND, ProtocolState.AUTOSAMPLE]:
                    self._async_raise_fsm_event(ProtocolEvent.NANO_TIME_SYNC)
        else:
            self.has_pps = False

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Process discover event
        @return next_state, next_agent_state
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        # key off the initialization flag to determine if we should sync the time
        if self._init_type == InitializationType.STARTUP:
            self._handler_time_sync()

        self._init_params()
        self._stop_autosample()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Process GET event
        @return next_state, result
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @return (next_state, result)
        @throws InstrumentParameterException
        """
        next_state = None
        result = None
        startup = False

        if len(args) < 1:
            raise InstrumentParameterException('Set command requires a parameter dict.')
        params = args[0]
        if len(args) > 1:
            startup = args[1]

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')
        if not isinstance(startup, bool):
            raise InstrumentParameterException('Startup not a bool.')

        self._set_params(params, startup)
        return next_state, result

    def _handler_command_start_direct(self):
        """
        Start direct access
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS, None)

    def _handler_command_start_autosample(self):
        """
        Start autosample
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_resp(InstrumentCommand.LILY_ON, expected_prompt=Prompt.LILY_ON)
        self._do_cmd_resp(InstrumentCommand.NANO_ON, expected_prompt=NANO_STRING)
        self._do_cmd_resp(InstrumentCommand.IRIS_ON, expected_prompt=Prompt.IRIS_ON)
        return ProtocolState.AUTOSAMPLE, (ResourceAgentState.STREAMING, 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_execute_direct(self, data):
        """
        Execute direct access command
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_direct(data)
        self._sent_cmds.append(data)
        return None, (None, None)

    def _handler_direct_access_stop_direct(self):
        """
        Stop direct access
        @return next_state, (next_agent_state, result)
        """
        next_state, next_agent_state = self._handler_unknown_discover()
        if next_state == DriverProtocolState.COMMAND:
            next_agent_state = ResourceAgentState.COMMAND

        return next_state, (next_agent_state, None)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter state handler
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit state handler
        """

    def _handler_acquire_status(self, *args, **kwargs):
        """
        We generate these particles here to avoid the chunker.  This allows us to process status
        messages with embedded messages from the other parts of the instrument.
        @return next_state, (next_agent_state, result)
        """
        ts = ntplib.system_to_ntp_time(time.time())
        parts = []

        for command, particle_class in [
            (InstrumentCommand.SYST_DUMP1, particles.SystStatusParticle),
            (InstrumentCommand.LILY_DUMP1, particles.LilyStatusParticle1),
            (InstrumentCommand.LILY_DUMP2, particles.LilyStatusParticle2),
            (InstrumentCommand.IRIS_DUMP1, particles.IrisStatusParticle1),
            (InstrumentCommand.IRIS_DUMP2, particles.IrisStatusParticle2),
            (InstrumentCommand.NANO_DUMP1, particles.NanoStatusParticle),
        ]:
            result, _ = self._do_cmd_resp(command, response_regex=particle_class.regex_compiled())
            parts.append(result)
        sample = self._extract_sample(particles.BotptStatusParticle,
                                      particles.BotptStatusParticle.regex_compiled(),
                                      NEWLINE.join(parts), ts)

        if self.get_current_state() == ProtocolState.AUTOSAMPLE:
            # acquiring status stops NANO output, restart it
            self._do_cmd_resp(InstrumentCommand.NANO_ON, expected_prompt=NANO_STRING)

        if not sample:
            raise InstrumentProtocolException('Failed to generate status particle')
        return None, (None, sample)

    def _handler_time_sync(self, *args, **kwargs):
        """
        Syncing time starts autosample...
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_resp(InstrumentCommand.NANO_SET_TIME, expected_prompt=NANO_STRING)
        if self.get_current_state() == ProtocolState.COMMAND:
            self._do_cmd_no_resp(InstrumentCommand.NANO_OFF)
        return None, (None, None)

    def _handler_start_leveling(self):
        """
        Send the start leveling command
        @return next_state, (next_agent_state, result)
        """
        if not self._param_dict.get(Parameter.LILY_LEVELING):
            self._schedule_leveling_timeout()
            self._do_cmd_resp(InstrumentCommand.LILY_START_LEVELING, expected_prompt=Prompt.LILY_START_LEVELING)
            self._param_dict.set_value(Parameter.LILY_LEVELING, True)
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_stop_leveling(self):
        """
        Send the stop leveling command
        @return next_state, (next_agent_state, result)
        """
        if self._param_dict.get(Parameter.LILY_LEVELING):
            self._remove_leveling_timeout()

            self._do_cmd_resp(InstrumentCommand.LILY_STOP_LEVELING, expected_prompt=Prompt.LILY_STOP_LEVELING)
            self._param_dict.set_value(Parameter.LILY_LEVELING, False)

            if self.get_current_state() == ProtocolState.AUTOSAMPLE:
                self._do_cmd_resp(InstrumentCommand.LILY_ON, expected_prompt=Prompt.LILY_ON)

            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, (None, None)

    def _handler_leveling_timeout(self):
        """
        Leveling has timed out, disable auto-relevel and mark leveling as failed.
        handler_stop_leveling will raise the config change event.
        @throws InstrumentProtocolException
        """
        self._param_dict.set_value(Parameter.AUTO_RELEVEL, False)
        self._param_dict.set_value(Parameter.LEVELING_FAILED, True)
        self._handler_stop_leveling()
        raise InstrumentProtocolException('Leveling failed to complete within timeout, disabling auto-relevel')

    def _handler_start_heater(self, *args, **kwargs):
        """
        Turn the heater on for Parameter.HEAT_DURATION hours
        @return next_state, (next_agent_state, result)
        """
        if not self._param_dict.get(Parameter.HEATER_ON):
            self._do_cmd_resp(InstrumentCommand.HEAT,
                              self._param_dict.get(Parameter.HEAT_DURATION),
                              response_regex=RegexResponse.HEAT)
            self._param_dict.set_value(Parameter.HEATER_ON, True)

            # Want to disable auto leveling when the heater is on
            self._param_dict.set_value(Parameter.AUTO_RELEVEL, False)

            self._schedule_heater_timeout()
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_stop_heater(self, *args, **kwargs):
        """
        Turn the heater on for Parameter.HEAT_DURATION hours
        @return next_state, (next_agent_state, result)
        """
        if self._param_dict.get(Parameter.HEATER_ON):
            self._do_cmd_resp(InstrumentCommand.HEAT,
                              0,
                              response_regex=RegexResponse.HEAT)
            self._param_dict.set_value(Parameter.HEATER_ON, False)
            self._remove_heater_timeout()
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_heater_timeout(self):
        """
        Heater should be finished.  Set HEATER_ON to false.
        """
        self._param_dict.set_value(Parameter.HEATER_ON, False)
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, None
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    
    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.ENTER, self._handler_unknown_enter)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.EXIT, self._handler_unknown_exit)
        self._protocol_fsm.add_handler(ProtocolState.UNKNOWN, ProtocolEvent.DISCOVER, self._handler_unknown_discover)

        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.ENTER, self._handler_command_enter)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.EXIT, self._handler_command_exit)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.START_DIRECT, self._handler_command_start_direct)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.CLOCK_SYNC, self._handler_sync_clock)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.GET, self._handler_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND, ProtocolEvent.SET, self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.ENTER, self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXIT, self._handler_direct_access_exit)
        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct)
        self._protocol_fsm.add_handler(ProtocolState.DIRECT_ACCESS, ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct)

        # Add build handlers for device commands.
        self._add_build_handler(Command.BATTERY, self._build_simple_command)

        # Add response handlers for device commands.
        self._add_response_handler(Command.BATTERY, self._parse_battery_response)
 
        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()
        
        self._chunker = StringChunker(Protocol.sieve_function)

        self._add_scheduler_event(ScheduledJob.CLOCK_SYNC, ProtocolEvent.CLOCK_SYNC)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples and status
        """
        matchers = []
        return_list = []

        matchers.append(RASFL_SampleDataParticle.regex_compiled())
                    
        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))
                    
        """
        if return_list != []:
            log.debug("sieve_function: raw_data=%s, return_list=%s" %(raw_data, return_list))
        """
        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        log.debug("_got_chunk: chunk=%s" %chunk)
        self._extract_sample(RASFL_SampleDataParticle, RASFL_SampleDataParticle.regex_compiled(), chunk, timestamp)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # implement virtual methods from base class.
    ########################################################################

    def apply_startup_params(self):
        """
        Apply sample_interval startup parameter.  
        """

        config = self.get_startup_config()
        log.debug("apply_startup_params: startup config = %s" %config)
        if config.has_key(Parameter.SAMPLE_INTERVAL):
            log.debug("apply_startup_params: setting sample_interval to %d" %config[Parameter.SAMPLE_INTERVAL])
            self._param_dict.set_value(Parameter.SAMPLE_INTERVAL, config[Parameter.SAMPLE_INTERVAL])

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_enter(self, *args, **kwargs):
        """
        Enter unknown state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_unknown_exit(self, *args, **kwargs):
        """
        Exit unknown state.
        """
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can only be COMMAND (instrument has no actual AUTOSAMPLE mode).
        @retval (next_state, result), (ProtocolState.COMMAND, None) if successful.
        """

        # force to command mode, this instrument has no autosample mode
        next_state = ProtocolState.COMMAND
        result = ResourceAgentState.COMMAND

        log.debug("_handler_unknown_discover: state = %s", next_state)
        return (next_state, result)


    ########################################################################
    # Command handlers.
    # just implemented to make DA possible, instrument has no actual command mode
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """

        # Command device to update parameters and send a config change event if needed.
        self._update_params()

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        """
        Exit command state.
        """
        pass

    def _handler_command_set(self, *args, **kwargs):
        """
        no writable parameters so does nothing, just implemented to make framework happy
        """

        next_state = None
        result = None
        return (next_state, result)

    def _handler_command_start_direct(self, *args, **kwargs):
        """
        """
        result = None
        next_state = ProtocolState.DIRECT_ACCESS
        next_agent_state = ResourceAgentState.DIRECT_ACCESS

        return (next_state, (next_agent_state, result))

    ########################################################################
    # Direct access handlers.
    ########################################################################

    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.                
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        
        self._sent_cmds = []
    
    def _handler_direct_access_exit(self, *args, **kwargs):
        """
        Exit direct access state.
        """
        pass

    def _handler_direct_access_execute_direct(self, data):
        next_state = None
        result = None

        self._do_cmd_direct(data)
                        
        return (next_state, result)

    def _handler_direct_access_stop_direct(self):
        result = None
        next_state = ProtocolState.COMMAND
        next_agent_state = ResourceAgentState.COMMAND

        return (next_state, (next_agent_state, result))

    ########################################################################
    # general handlers.
    ########################################################################

    def _handler_sync_clock(self, *args, **kwargs):
        """
        sync clock close to a second edge 
        @retval (next_state, (next_agent_state, result)) tuple, (None, (None, None)).
        @throws InstrumentTimeoutException if device respond correctly.
        @throws InstrumentProtocolException if command could not be built or misunderstood.
        """

        next_state = None
        next_agent_state = None
        result = None

        time_format = "%Y/%m/%d %H:%M:%S"
        str_val = get_timestamp_delayed(time_format)
        log.debug("Setting instrument clock to '%s'", str_val)
        self._do_cmd_resp(Command.STOP, expected_prompt=Prompt.STOPPED)
        try:
            self._do_cmd_resp(Command.SET_CLOCK, str_val, expected_prompt=Prompt.CR_NL)
        finally:
            # ensure that we try to start the instrument sampling again
            self._do_cmd_resp(Command.GO, expected_prompt=Prompt.GO)

        return (next_state, (next_agent_state, result))

    ########################################################################
    # Private helpers.
    ########################################################################

    def _wakeup(self, wakeup_timeout=10, response_timeout=3):
        """
        Over-ridden because waking this instrument up is a multi-step process with
        two different requests required
        @param wakeup_timeout The timeout to wake the device.
        @param response_timeout The time to look for response to a wakeup attempt.
        @throw InstrumentTimeoutException if the device could not be woken.
        """
        sleep_time = .1
        command = Command.END_OF_LINE
        
        # Grab start time for overall wakeup timeout.
        starttime = time.time()
        
        while True:
            # Clear the prompt buffer.
            log.debug("_wakeup: clearing promptbuf: %s" % self._promptbuf)
            self._promptbuf = ''
        
            # Send a command and wait delay amount for response.
            log.debug('_wakeup: Sending command %s, delay=%s' %(command.encode("hex"), response_timeout))
            for char in command:
                self._connection.send(char)
                time.sleep(INTER_CHARACTER_DELAY)
            sleep_amount = 0
            while True:
                time.sleep(sleep_time)
                if self._promptbuf.find(Prompt.COMMAND_INPUT) != -1:
                    # instrument is awake
                    log.debug('_wakeup: got command input prompt %s' % Prompt.COMMAND_INPUT)
                    # add inter-character delay which _do_cmd_resp() incorrectly doesn't add to the start of a transmission
                    time.sleep(INTER_CHARACTER_DELAY)
                    return Prompt.COMMAND_INPUT
                if self._promptbuf.find(Prompt.ENTER_CTRL_C) != -1:
                    command = Command.CONTROL_C
                    break
                if self._promptbuf.find(Prompt.PERIOD) == 0:
                    command = Command.CONTROL_C
                    break
                sleep_amount += sleep_time
                if sleep_amount >= response_timeout:
                    log.debug("_wakeup: expected response not received, buffer=%s" % self._promptbuf)
                    break

            if time.time() > starttime + wakeup_timeout:
                raise InstrumentTimeoutException("_wakeup(): instrument failed to wakeup in %d seconds time" %wakeup_timeout)

    
    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """
        self._cmd_dict.add(Capability.CLOCK_SYNC, display_name="synchronize clock")

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with XR-420 parameters.
        For each parameter key add value formatting function for set commands.
        """
        # The parameter dictionary.
        self._param_dict = ProtocolParameterDict()
        
        # Add parameter handlers to parameter dictionary for instrument configuration parameters.
        self._param_dict.add(Parameter.BATTERY,
                             r'Battery: (.*)V', 
                             lambda match : match.group(1),
                             lambda string : str(string),
                             type=ParameterDictType.STRING,
                             display_name="battery",
                             expiration=0,
                             visibility=ParameterDictVisibility.READ_ONLY)

        self._param_dict.add(Parameter.SAMPLE_INTERVAL,
                             r'Not used. This parameter is not parsed from instrument response',
                             None,
                             self._int_to_string,
                             type=ParameterDictType.INT,
                             default_value=30,
                             value=30,
                             startup_param=True,
                             display_name="sample_interval",
                             visibility=ParameterDictVisibility.IMMUTABLE)

    def _do_cmd_resp(self, cmd, *args, **kwargs):
        CommandResponseInstrumentProtocol._do_cmd_resp(self, cmd, args, kwargs, write_delay=INTER_CHARACTER_DELAY)

    
    def _update_params(self, *args, **kwargs):
        """
        Update the parameter dictionary. 
        """
        
        log.debug("_update_params:")
        self._do_cmd_resp(Command.BATTERY)

    def _parse_battery_response(self, response, prompt):
        """
        Parse handler for battery command.
        @param response command response string.
        @param prompt prompt following command response.        
        @throws InstrumentProtocolException if clock command misunderstood.
        """
        log.debug("_parse_battery_response: response=%s, prompt=%s" %(response, prompt))
        if prompt == Prompt.UNRECOGNIZED_COMMAND: 
            raise InstrumentProtocolException('battery command not recognized: %s.' % response)

        if not self._param_dict.update(response):
            raise InstrumentProtocolException('battery command not parsed: %s.' % response)

        return
Beispiel #17
0
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)
Beispiel #18
0
class Protocol(InstrumentProtocol):
    __metaclass__ = META_LOGGER

    def __init__(self, driver_event):
        super(Protocol, self).__init__(driver_event)
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        handlers = {
            ProtocolState.UNKNOWN: (
                (ProtocolEvent.ENTER, self._handler_unknown_enter),
                (ProtocolEvent.EXIT, self._handler_unknown_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ),
            ProtocolState.AUTOSAMPLE: (
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_autosample_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.FLUSH, self._flush),
                (ProtocolEvent.CONFIG_ERROR, self._handler_config_error),
            ),
            ProtocolState.WRITE_ERROR: (
                (ProtocolEvent.ENTER, self._handler_error_enter),
                (ProtocolEvent.EXIT, self._handler_error_exit),
            ),
            ProtocolState.CONFIG_ERROR: (
                (ProtocolEvent.ENTER, self._handler_error_enter),
                (ProtocolEvent.EXIT, self._handler_error_exit),
            )
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Build dictionaries for driver schema
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)
        self._logs = {}
        self._filled_logs = []
        self._pickle_cache = []

        self._persistent_store = None

        # lock for flush actions to prevent writing or altering the data files
        # during flush
        self._lock = Lock()
        self._pktid = None

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.GET, display_name="Get")
        self._cmd_dict.add(Capability.SET, display_name="Set")
        self._cmd_dict.add(Capability.DISCOVER, display_name="Discover")

    def _build_param_dict(self):
        self._param_dict.add(Parameter.REFDES,
                             'NA',
                             str,
                             str,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             display_name='Reference Designator',
                             description='Reference Designator for this driver',
                             type=ParameterDictType.STRING)
        self._param_dict.add(Parameter.SOURCE_REGEX,
                             'NA',
                             str,
                             str,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             display_name='Source Filter Regex',
                             description='Filter sources to be processed from the ORB',
                             type=ParameterDictType.STRING,
                             value_description='Regular expression')
        self._param_dict.add(Parameter.FILE_LOCATION,
                             'NA',
                             str,
                             str,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             default_value="./antelope_data",
                             display_name='File Location',
                             description='Root file path of the packet data files',
                             type=ParameterDictType.STRING,
                             value_description='String representing the packet data root file path')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_persistent_dict(self):
        refdes = self._param_dict.get(Parameter.REFDES)

        self._persistent_store = ConsulPersistentStore(refdes)
        if 'pktid' not in self._persistent_store:
            self._persistent_store['pktid'] = ORBOLDEST

    def _handler_set(self, *args, **kwargs):
        pass

    def _update_params(self, *args, **kwargs):
        pass

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        self._verify_not_readonly(*args, **kwargs)

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Set the base directory for the packet data file location.
        PacketLog.base_dir = self._param_dict.get(Parameter.FILE_LOCATION)

    def _flush(self):
        particles = []
        with self._lock:
            for _log in self._logs.itervalues():
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (ProtocolState.WRITE_ERROR, None)

                particles.append(AntelopeMetadataParticle(_log, preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP))

            for _log in self._filled_logs:
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (ProtocolState.WRITE_ERROR, None)

                particles.append(AntelopeMetadataParticle(_log, preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP))
                _log.data = []

            self._filled_logs = []
            if self._pktid is not None:
                log.info('updating persistent store')
                self._persistent_store['pktid'] = self._pktid

        for particle in particles:
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())

        return None, (None, None)

    # noinspection PyProtectedMember
    def _orbstart(self):
        self._connection._command_port_agent('orbselect %s' % self._param_dict.get(Parameter.SOURCE_REGEX))
        self._connection._command_port_agent('orbseek %s' % self._persistent_store['pktid'])
        self._connection._command_port_agent('orbstart')

    # noinspection PyProtectedMember
    def _orbstop(self):
        self._connection._command_port_agent('orbstop')

    def stop_scheduled_job(self, schedule_job):
        """
        Remove the scheduled job
        @param schedule_job scheduling job.
        """
        if self._scheduler is not None:
            try:
                self._remove_scheduler(schedule_job)
            except KeyError:
                log.warn("_remove_scheduler could not find %s", schedule_job)

    def got_data(self, port_agent_packet):
        data_length = port_agent_packet.get_data_length()
        data_type = port_agent_packet.get_header_type()

        if data_type == PortAgentPacket.PICKLED_FROM_INSTRUMENT:
            self._pickle_cache.append(port_agent_packet.get_data())
            # this is the max size (65535) minus the header size (16)
            # any packet of this length will be followed by one or more packets
            # with additional data. Keep accumulating packets until we have
            # the complete data, then unpickle.
            if data_length != 65519:
                data = pickle.loads(''.join(self._pickle_cache))
                self._pickle_cache = []
                self._bin_data(data)
        else:
            raise InstrumentProtocolException('Received unpickled data from port agent')

    def got_raw(self, port_agent_packet):
        pass

    def _get_bin(self, packet):
        rate_map = {
            1: 86400,       # 1 day
            8: 86400,       # 1 day
            40: 86400,      # 1 day
            200: 86400,     # 1 day
            64000: 60 * 5,  # 5 minutes
            256000: 60,     # 1 minute
        }
        start_time = packet['time']
        rate = packet['samprate']
        bin_size = rate_map.get(rate, 60)
        bin_value = int(start_time/bin_size)
        bin_start = bin_value * bin_size
        bin_end = (bin_value + 1) * bin_size

        return bin_start, bin_end

    def _bin_data(self, packet):
        key = '%s.%s.%s.%s' % (packet['net'], packet.get('location', ''),
                               packet.get('sta', ''), packet['chan'])
        start, end = self._get_bin(packet)

        with self._lock:
            self._pktid = packet['pktid']

            if key not in self._logs:
                self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))

            try:
                while True:
                    packet = self._logs[key].add_packet(packet)
                    if packet is None:
                        break
                    # residual, we need a new bin
                    # log is complete, move to holding list until next flush
                    self._filled_logs.append(self._logs[key])
                    del self._logs[key]
                    # create the new log...
                    start, end = self._get_bin(packet)
                    self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))

            except GapException:
                # non-contiguous data detected, close this log and open a new one
                self._filled_logs.append(self._logs[key])
                del self._logs[key]
                # create the new log
                self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))
                self._logs[key].add_packet(packet)

    ########################################################################
    # 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)
        self._add_scheduler_event(ScheduledJob.DISCOVER, ProtocolEvent.DISCOVER)

    def _handler_unknown_exit(self, *args, **kwargs):
        """
        Exit unknown state.
        """
        self.stop_scheduled_job(ScheduledJob.DISCOVER)

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; always COMMAND.
        @return protocol_state, protocol_state
        """
        next_state = None
        if self._connection:
            next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ######################################################
    # AUTOSAMPLE handlers
    ######################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        try:
            self._init_params()
            self._build_persistent_dict()
            self._add_scheduler_event(ScheduledJob.FLUSH, ProtocolEvent.FLUSH)
            self._orbstart()

        except InstrumentProtocolException:
            self._async_raise_fsm_event(ProtocolEvent.CONFIG_ERROR)

        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit autosample state.
        """
        self.stop_scheduled_job(ScheduledJob.FLUSH)
        self._orbstop()

    def _handler_config_error(self, *args, **kwargs):
        next_state = ProtocolState.CONFIG_ERROR
        result = None
        return next_state, (next_state, result)

    ######################################################
    # ERROR handlers
    ######################################################

    def _handler_error_enter(self, *args, **kwargs):
        """
        Enter error state.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_error_exit(self, *args, **kwargs):
        """
        Exit error state.
        This should never occur, this state is a dead end.
        """
        pass
Beispiel #19
0
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
Beispiel #20
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.STOP_AUTOSAMPLE,
                 self._handler_autosample_stop_autosample),
                (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_AUTOSAMPLE,
                 self._handler_command_start_autosample),
                (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Construct the metadata dictionaries
        self._build_command_dict()
        self._build_driver_dict()

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # set up scheduled event handling
        self.initialize_scheduler()
        self._schedulers = []

    def _generate_particle(self, stream_name, count=1):
        # we're faking it anyway, send these as fast as we can...
        # the overall rate will be close enough
        particle = VirtualParticle(stream_name, port_timestamp=0)
        for x in range(count):
            particle.contents['port_timestamp'] = ntplib.system_to_ntp_time(
                time.time())
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())
            time.sleep(.001)

    def _create_scheduler(self, stream_name, rate):
        job_name = stream_name

        if rate > 1:
            interval = 1
        else:
            interval = 1 / rate

        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE:
                        TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: interval
                    }
                }
            }
        }
        self.set_init_params(config)
        self._schedulers.append(stream_name)

        if rate > 1:
            self._add_scheduler(
                stream_name,
                functools.partial(self._generate_particle,
                                  stream_name,
                                  count=rate))
        else:
            self._add_scheduler(
                stream_name,
                functools.partial(self._generate_particle, stream_name))

    def _delete_all_schedulers(self):
        for name in self._schedulers:
            try:
                self._remove_scheduler(name)
            except:
                pass

    def _got_chunk(self, chunk, ts):
        """
        Process chunk output by the chunker.  Generate samples and (possibly) react
        @param chunk: data
        @param ts: ntp timestamp
        @return sample
        @throws InstrumentProtocolException
        """
        return

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE,
                           display_name="Start Autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE,
                           display_name="Stop Autosample")

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _update_params(self, *args, **kwargs):
        """
        Update the param dictionary based on instrument response
        """

    def _set_params(self, *args, **kwargs):
        if len(args) < 1:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')
        params = args[0]

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        self._param_dict = ProtocolParameterDict()

        for param in params:
            log.info('Creating new parameter: %s', param)
            self._param_dict.add(param, '', None, None)
            self._param_dict.set_value(param, params[param])

    def set_init_params(self, config):
        if not isinstance(config, dict):
            raise InstrumentParameterException("Invalid init config format")

        self._startup_config = config

        param_config = config.get(DriverConfigKey.PARAMETERS)
        if param_config:
            for name in param_config.keys():
                self._param_dict.add(name, '', None, None)
                log.debug("Setting init value for %s to %s", name,
                          param_config[name])
                self._param_dict.set_init_value(name, param_config[name])

    def _very_long_command(self, *args, **kwargs):
        return None, time.sleep(30)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Process discover event
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self._init_params()

        for stream_name in self._param_dict.get_keys():
            self._create_scheduler(stream_name,
                                   self._param_dict.get(stream_name))
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._delete_all_schedulers()
        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Process GET event
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @throws InstrumentParameterException
        """
        next_state = None
        result = []
        self._set_params(*args, **kwargs)
        return next_state, (next_state, result)

    def _handler_command_start_autosample(self):
        """
        Start autosample
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter state handler
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
Beispiel #21
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    __metaclass__ = get_logging_metaclass(log_level='trace')

    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.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.START_AUTOSAMPLE,
                                       self._handler_command_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ACQUIRE_STATUS,
                                       self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.GET,
                                       self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.SET,
                                       self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.STOP_AUTOSAMPLE,
                                       self._handler_autosample_stop)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.GET,
                                       self._handler_command_get)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add sample handlers.

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(self.sieve_function)

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        self._param_dict.add(
            Parameter.SCHEDULE,
            r'schedule:\s+(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="Schedule",
            description=
            "Large block of text used to create the .yaml file defining the sampling schedule.",
            startup_param=True,
            default_value=yaml.dump(DEFAULT_CONFIG, default_flow_style=False))

        self._param_dict.add(
            Parameter.FTP_IP_ADDRESS,
            r'ftp address:\s+(\d\d\d\d\.\d\d\d\d\.\d\d\d\d\.\d\d\d)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP IP Address",
            description=
            "IP address the driver uses to connect to the instrument FTP server.",
            startup_param=True,
            default_value=DEFAULT_HOST)

        self._param_dict.add(
            Parameter.FTP_USERNAME,
            r'username:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP User Name",
            description="Username used to connect to the FTP server.",
            startup_param=True,
            default_value=USER_NAME)

        self._param_dict.add(
            Parameter.FTP_PASSWORD,
            r'password:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Password",
            description="Password used to connect to the FTP server.",
            startup_param=True,
            default_value=PASSWORD)

        self._param_dict.add(
            Parameter.FTP_PORT,
            r'port:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Port",
            description=
            "Location on the OOI infrastructure where .raw files and echogram images will be stored.",
            startup_param=True,
            default_value=DEFAULT_PORT)

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_STATUS,
                           display_name="Acquire Status")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # 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
        @retval next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Try to get the status to check if the instrument is alive
        host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
        port = self._param_dict.get_config_value(Parameter.FTP_PORT)
        response = self._url_request(host, port, '/status.json')

        if response is None:
            error_msg = "_handler_unknown_discover: Unable to connect to host: %s" % host
            log.error(error_msg)
            raise InstrumentConnectionException(error_msg)

        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        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_get(self, *args, **kwargs):
        """
        Get parameters while in the command state.
        @param params List of the parameters to pass to the state
        @retval returns (next_state, result) where result is a dict {}. No
            agent state changes happening with Get, so no next_agent_state
        @throw InstrumentParameterException for invalid parameter
        """
        result_vals = {}

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException(
                '_handler_command_get requires a parameter dict.')

        if Parameter.ALL in params:
            log.debug("Parameter ALL in params")
            params = Parameter.list()
            params.remove(Parameter.ALL)

        log.debug("_handler_command_get: params = %s", params)

        if params is None or not isinstance(params, list):
            raise InstrumentParameterException(
                "GET parameter list not a list!")

        # fill the return values from the update
        for param in params:
            if not Parameter.has(param):
                raise InstrumentParameterException("Invalid parameter!")
            result_vals[param] = self._param_dict.get(param)
            self._param_dict.get_config_value(param)
        result = result_vals

        log.debug("Get finished, next_state: %s, result: %s", None, result)
        return None, result

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        @retval next state, result
        """
        startup = False

        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                '_handler_command_set: command requires a parameter dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        # For each key, val in the params, set the param dictionary.
        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, None

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        # verify param is not readonly param
        self._verify_not_readonly(*args, **kwargs)

        for key, val in params.iteritems():
            log.debug("KEY = %s VALUE = %s", key, val)
            self._param_dict.set_value(key, val)
            if key == Parameter.SCHEDULE:
                self._ftp_schedule_file()

                # Load the schedule file
                host = self._param_dict.get(Parameter.FTP_IP_ADDRESS)
                port = self._param_dict.get_config_value(Parameter.FTP_PORT)
                log.debug("_set_params: stop the current schedule file")
                self._url_request(host, port, '/stop_schedule', data={})
                log.debug("_set_params: upload driver YAML file to host %s",
                          host)
                res = self._url_request(host,
                                        port,
                                        '/load_schedule',
                                        data=json.dumps(
                                            {'filename': YAML_FILE_NAME}))
                log.debug("_set_params: result from load = %s", res)

        log.debug("set complete, update params")

    def _ftp_schedule_file(self):
        """
        Construct a YAML schedule file and
        ftp the file to the Instrument server
        """
        # Create a temporary file and write the schedule YAML information to the file
        try:
            config_file = tempfile.TemporaryFile()
            log.debug("temporary file created")

            if config_file is None or not isinstance(config_file, file):
                raise InstrumentException("config_file is not a temp file!")

            config_file.write(self._param_dict.get(Parameter.SCHEDULE))
            config_file.seek(0)
            log.debug("finished writing config file:\n%r",
                      self._param_dict.get(Parameter.SCHEDULE))

        except Exception as e:
            log.error("Create schedule YAML file exception: %s", e)
            raise e

        #  FTP the schedule file to the ZPLSC server
        host = ''

        try:
            log.debug("Create a ftp session")
            host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
            log.debug("Got host ip address %s", host)

            ftp_session = ftplib.FTP()
            ftp_session.connect(host)
            ftp_session.login(USER_NAME, PASSWORD)
            log.debug("ftp session was created...")

            ftp_session.set_pasv(False)
            ftp_session.cwd("config")

            ftp_session.storlines('STOR ' + YAML_FILE_NAME, config_file)
            files = ftp_session.dir()

            log.debug("*** Config yaml file sent: %s", files)

            ftp_session.quit()
            config_file.close()

        except (ftplib.socket.error, ftplib.socket.gaierror), e:
            log.error("ERROR: cannot reach FTP Host %s: %s ", host, e)
            raise InstrumentException("ERROR: cannot reach FTP Host %s " %
                                      host)

        log.debug("*** FTP %s to ftp host %s successfully", YAML_FILE_NAME,
                  host)
Beispiel #22
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample),
                (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample),
                (ProtocolEvent.VERY_LONG_COMMAND, self._very_long_command),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Construct the metadata dictionaries
        self._build_command_dict()
        self._build_driver_dict()

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # set up scheduled event handling
        self.initialize_scheduler()
        self._schedulers = []

    def _generate_particle(self, stream_name, count=1):
        # we're faking it anyway, send these as fast as we can...
        # the overall rate will be close enough
        particle = VirtualParticle(stream_name, port_timestamp=0)
        for x in range(count):
            particle.contents["port_timestamp"] = ntplib.system_to_ntp_time(time.time())
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())
            time.sleep(0.001)

    def _create_scheduler(self, stream_name, rate):
        job_name = stream_name

        if rate > 1:
            interval = 1
        else:
            interval = 1 / rate

        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: interval,
                    }
                }
            }
        }
        self.set_init_params(config)
        self._schedulers.append(stream_name)

        if rate > 1:
            self._add_scheduler(stream_name, functools.partial(self._generate_particle, stream_name, count=rate))
        else:
            self._add_scheduler(stream_name, functools.partial(self._generate_particle, stream_name))

    def _delete_all_schedulers(self):
        for name in self._schedulers:
            try:
                self._remove_scheduler(name)
            except:
                pass

    def _got_chunk(self, chunk, ts):
        """
        Process chunk output by the chunker.  Generate samples and (possibly) react
        @param chunk: data
        @param ts: ntp timestamp
        @return sample
        @throws InstrumentProtocolException
        """
        return

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample")

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _update_params(self, *args, **kwargs):
        """
        Update the param dictionary based on instrument response
        """

    def _set_params(self, *args, **kwargs):
        if len(args) < 1:
            raise InstrumentParameterException("Set command requires a parameter dict.")
        params = args[0]

        if not isinstance(params, dict):
            raise InstrumentParameterException("Set parameters not a dict.")

        self._param_dict = ProtocolParameterDict()

        for param in params:
            log.info("Creating new parameter: %s", param)
            self._param_dict.add(param, "", None, None)
            self._param_dict.set_value(param, params[param])

    def set_init_params(self, config):
        if not isinstance(config, dict):
            raise InstrumentParameterException("Invalid init config format")

        self._startup_config = config

        param_config = config.get(DriverConfigKey.PARAMETERS)
        if param_config:
            for name in param_config.keys():
                self._param_dict.add(name, "", None, None)
                log.debug("Setting init value for %s to %s", name, param_config[name])
                self._param_dict.set_init_value(name, param_config[name])

    def _very_long_command(self, *args, **kwargs):
        return None, time.sleep(30)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Process discover event
        """
        next_state = ProtocolState.COMMAND
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self._init_params()

        for stream_name in self._param_dict.get_keys():
            self._create_scheduler(stream_name, self._param_dict.get(stream_name))
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._delete_all_schedulers()
        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Process GET event
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @throws InstrumentParameterException
        """
        next_state = None
        result = []
        self._set_params(*args, **kwargs)
        return next_state, (next_state, result)

    def _handler_command_start_autosample(self):
        """
        Start autosample
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter state handler
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
class 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()
Beispiel #24
0
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 SatlanticOCR507InstrumentProtocol(CommandResponseInstrumentProtocol):
    """The instrument protocol classes to deal with a Satlantic OCR507 sensor.
    The protocol is a very simple command/response protocol with a few show
    commands and a few set commands.
    Note protocol state machine must be called "self._protocol_fsm"
    """
    _data_particle_type = SatlanticOCR507DataParticle
    _config_particle_type = SatlanticOCR507ConfigurationParticle
    _data_particle_regex = SAMPLE_REGEX
    _config_particle_regex = CONFIG_REGEX

    __metaclass__ = get_logging_metaclass(log_level='debug')

    def __init__(self, callback=None):
        CommandResponseInstrumentProtocol.__init__(self, Prompt, EOLN, callback)

        self._last_data_timestamp = None

        self._protocol_fsm = ThreadSafeFSM(SatlanticProtocolState, SatlanticProtocolEvent, SatlanticProtocolEvent.ENTER,
                                           SatlanticProtocolEvent.EXIT)

        self._protocol_fsm.add_handler(SatlanticProtocolState.UNKNOWN, SatlanticProtocolEvent.ENTER,
                                       self._handler_unknown_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.UNKNOWN, SatlanticProtocolEvent.DISCOVER,
                                       self._handler_unknown_discover)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.ENTER,
                                       self._handler_command_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.GET,
                                       self._handler_command_get)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.SET,
                                       self._handler_command_set)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.START_AUTOSAMPLE,
                                       self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.ACQUIRE_STATUS,
                                       self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND, SatlanticProtocolEvent.START_DIRECT,
                                       self._handler_command_start_direct)
        self._protocol_fsm.add_handler(SatlanticProtocolState.AUTOSAMPLE, SatlanticProtocolEvent.ENTER,
                                       self._handler_autosample_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.AUTOSAMPLE, SatlanticProtocolEvent.STOP_AUTOSAMPLE,
                                       self._handler_autosample_stop_autosample)
        self._protocol_fsm.add_handler(SatlanticProtocolState.DIRECT_ACCESS, SatlanticProtocolEvent.ENTER,
                                       self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.DIRECT_ACCESS, SatlanticProtocolEvent.EXECUTE_DIRECT,
                                       self._handler_direct_access_execute_direct)
        self._protocol_fsm.add_handler(SatlanticProtocolState.DIRECT_ACCESS, SatlanticProtocolEvent.STOP_DIRECT,
                                       self._handler_direct_access_stop_direct)

        self._protocol_fsm.start(SatlanticProtocolState.UNKNOWN)

        self._add_response_handler(Command.GET, self._parse_get_response)
        self._add_response_handler(Command.SET, self._parse_set_response)
        self._add_response_handler(Command.INVALID, self._parse_invalid_response)

        self._param_dict.add(Parameter.MAX_RATE,
                             r"Maximum\ Frame\ Rate:\ (\S+).*?\s*",
                             lambda match: match.group(1),
                             lambda sVal: '%s' % sVal,
                             type=ParameterDictType.STRING,
                             display_name="Max Rate",
                             default_value='0',
                             startup_param=True,
                             direct_access=True)

        self._param_dict.add(Parameter.INIT_AT,
                             r"Initialize Automatic Telemetry: (off|on)",
                             lambda match: True if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Init AT",
                             default_value=True,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._param_dict.add(Parameter.INIT_SM,
                             r"Initialize Silent Mode: (off|on)",
                             lambda match: True if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Init SM",
                             default_value=True,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._param_dict.add(Parameter.NET_MODE,
                             r"Network Mode: (off|on)",
                             lambda match: True if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Net Mode",
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._cmd_dict.add(SatlanticCapability.START_AUTOSAMPLE, display_name="Start Autosample")
        self._cmd_dict.add(SatlanticCapability.STOP_AUTOSAMPLE, display_name="Stop Autosample")
        self._cmd_dict.add(SatlanticCapability.ACQUIRE_STATUS, display_name="Acquire Status")

        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

        self._chunker = StringChunker(self.sieve_function)

    @staticmethod
    def _boolean_to_off_on(v):
        """
        Write a boolean value to string formatted for sbe16 set operations.
        @param v a boolean value.
        @retval A yes/no string formatted for sbe16 set operations.
        @throws InstrumentParameterException if value not a bool.
        """

        if not isinstance(v, bool):
            raise InstrumentParameterException('Value %s is not a bool.' % str(v))
        if v:
            return 'on'
        return 'off'


    @staticmethod
    def sieve_function(raw_data):
        """ The method that splits samples
        """
        log.debug("Raw Data: %r, len: %d", raw_data, len(raw_data))
        log.debug(SAMPLE_REGEX.pattern)
        matchers = [SAMPLE_REGEX, CONFIG_REGEX]
        return_list = []

        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):
        """
        """
        events_out = [x for x in events if SatlanticCapability.has(x)]
        return events_out

    def get_config(self, *args, **kwargs):
        """ Get the entire configuration for the instrument

        @param params The parameters and values to set
        @retval None if nothing was done, otherwise result of FSM event handle
        Should be a dict of parameters and values
        """
        for param in Parameter.list():
            if param != Parameter.ALL:
                self._do_cmd_resp(Command.GET, param, **kwargs)

    def _do_cmd(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers.

        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @retval The fully built command that was sent
        """
        expected_prompt = kwargs.get('expected_prompt', None)
        cmd_line = self._build_default_command(cmd, *args)

        # Send command.
        log.debug('_do_cmd: %s, length=%s' % (repr(cmd_line), len(cmd_line)))
        if len(cmd_line) == 1:
            self._connection.send(cmd_line)
        else:
            for char in cmd_line:
                starttime = time.time()
                self._connection.send(char)
                while len(self._promptbuf) == 0 or char not in self._promptbuf[-1]:
                    time.sleep(0.0015)
                    if time.time() > starttime + 3:
                        break

            time.sleep(0.115)
            starttime = time.time()
            self._connection.send(EOLN)
            while EOLN not in self._promptbuf[len(cmd_line):len(cmd_line) + 2]:
                time.sleep(0.0015)
                if time.time() > starttime + 3:
                    break

            # Limit resend_check_value from expected_prompt to one of the two below
            resend_check_value = None
            if expected_prompt is not None:
                for check in (Prompt.COMMAND, "SATDI7"):
                    if check in expected_prompt:
                        log.trace('_do_cmd: command: %s, check=%s' % (cmd_line, check))
                        resend_check_value = check

            # Resend the EOLN if it did not go through the first time
            starttime = time.time()
            if resend_check_value is not None:
                while True:
                    time.sleep(0.1)
                    if time.time() > starttime + 2:
                        log.debug("Sending eoln again.")
                        self._connection.send(EOLN)
                        starttime = time.time()
                    if resend_check_value in self._promptbuf:
                        break
                    if Prompt.INVALID_COMMAND in self._promptbuf:
                        break

        return cmd_line

    def _do_cmd_no_resp(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers. No response is handled as a result of the command.
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        """
        self._do_cmd(cmd, *args, **kwargs)

    def _do_cmd_resp(self, cmd, *args, **kwargs):
        """
        Perform a command-response on the device.
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @param expected_prompt kwarg offering a specific prompt to look for
        other than the ones in the protocol class itself.
        @param response_regex kwarg with a compiled regex for the response to
        match. Groups that match will be returned as a string.
        Cannot be supplied with expected_prompt. May be helpful for instruments that do not have a prompt.
        @retval resp_result The (possibly parsed) response result including the
        first instance of the prompt matched. If a regex was used, the prompt
        will be an empty string and the response will be the joined collection of matched groups.
        @raises InstrumentCommandException if the response did not occur in time.
        @raises InstrumentProtocolException if command could not be built or if response was not recognized.
        """
        timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT)
        expected_prompt = kwargs.get('expected_prompt', [Prompt.INVALID_COMMAND, Prompt.USAGE, Prompt.COMMAND])
        response_regex = kwargs.get('response_regex', None)

        if response_regex and not isinstance(response_regex, RE_PATTERN):
            raise InstrumentProtocolException('Response regex is not a compiled pattern!')

        if expected_prompt and response_regex:
            raise InstrumentProtocolException('Cannot supply both regex and expected prompt!')

        retry_count = 5
        retry_num = 0
        cmd_line = ""
        result = ""
        prompt = ""
        for retry_num in xrange(retry_count):
            # Clear line and prompt buffers for result.
            self._linebuf = ''
            self._promptbuf = ''

            cmd_line = self._do_cmd(cmd, *args, **kwargs)

            # Wait for the prompt, prepare result and return, timeout exception
            if response_regex:
                result_tuple = self._get_response(timeout, response_regex=response_regex,
                                                  expected_prompt=expected_prompt)
                result = "".join(result_tuple)
            else:
                (prompt, result) = self._get_response(timeout, expected_prompt=expected_prompt)

            # Confirm the entire command was sent, otherwise resend retry_count number of times
            if len(cmd_line) > 1 and \
                    (expected_prompt is not None or
                         (response_regex is not None)) \
                    and cmd_line not in result:
                log.debug('_do_cmd_resp: Send command: %s failed %s attempt, result = %s.', cmd, retry_num, result)
                if retry_num >= retry_count:
                    raise InstrumentCommandException('_do_cmd_resp: Failed %s attempts sending command: %s' %
                                                     (retry_count, cmd))
            else:
                break

        log.debug('_do_cmd_resp: Sent command: %s, %s reattempts, expected_prompt=%s, result=%r.',
                  cmd_line, retry_num, expected_prompt, result)

        resp_handler = self._response_handlers.get((self.get_current_state(), cmd), None) or \
                       self._response_handlers.get(cmd, None)
        resp_result = None
        if resp_handler:
            resp_result = resp_handler(result, prompt)

        time.sleep(0.3)  # give some time for the instrument connection to keep up

        return resp_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)

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can be COMMAND or AUTOSAMPLE.
        @retval (next_state, result), (SatlanticProtocolState.COMMAND, ResourceAgentState.IDLE or
        SatlanticProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING) if successful.
        """
        try:
            invalidCommandResponse = self._do_cmd_resp(Command.INVALID, timeout=3,
                                                       expected_prompt=Prompt.INVALID_COMMAND)
        except InstrumentTimeoutException as ex:
            invalidCommandResponse = None  # The instrument is not in COMMAND: it must be polled or AUTOSAMPLE

        log.debug("_handler_unknown_discover: returned: %s", invalidCommandResponse)
        if invalidCommandResponse:
            return SatlanticProtocolState.COMMAND, ResourceAgentState.IDLE
        # Put the instrument back into full autosample
        self._do_cmd_no_resp(Command.SWITCH_TO_AUTOSAMPLE)
        return SatlanticProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        # Command device to update parameters and send a config change event.
        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_get(self, *args, **kwargs):
        """Handle getting data from command mode

        @param params List of the parameters to pass to the state
        @retval return (next state, result)
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """Handle setting data from command mode

        @param params Dict of the parameters and values to pass to the state
        @return (next state, result)
        """
        self._set_params(*args, **kwargs)
        return None, None

    def _handler_command_start_autosample(self, params=None, *args, **kwargs):
        """
        Handle getting an start autosample event when in command mode
        @param params List of the parameters to pass to the state
        @return next state (next agent state, result)
        """
        result = None

        self._do_cmd_no_resp(Command.EXIT_AND_RESET)
        time.sleep(RESET_DELAY)
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        next_state = SatlanticProtocolState.AUTOSAMPLE
        next_agent_state = ResourceAgentState.STREAMING

        return next_state, (next_agent_state, result)

    def _handler_command_start_direct(self):
        """
        """
        result = None

        next_state = SatlanticProtocolState.DIRECT_ACCESS
        next_agent_state = ResourceAgentState.DIRECT_ACCESS

        log.debug("_handler_command_start_direct: entering DA mode")
        return next_state, (next_agent_state, result)

    def _handler_command_acquire_status(self, *args, **kwargs):
        """
        Handle SatlanticProtocolState.COMMAND SatlanticProtocolEvent.ACQUIRE_STATUS

        @return next state (next agent state, result)
        """
        next_state = None
        next_agent_state = None
        result = None

        self._do_cmd_no_resp(Command.ID)
        self._do_cmd_no_resp(Command.SHOW_ALL)

        return next_state, (next_agent_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Handle SatlanticProtocolState.AUTOSAMPLE SatlanticProtocolEvent.ENTER

        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None

        if not self._confirm_autosample_mode:
            raise InstrumentProtocolException(error_code=InstErrorCode.HARDWARE_ERROR,
                                              msg="Not in the correct mode!")

        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        return next_state, result

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """Handle SatlanticProtocolState.AUTOSAMPLE stop

        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None

        try:
            self._send_break()
            next_state = SatlanticProtocolState.COMMAND
            next_agent_state = ResourceAgentState.COMMAND
        except InstrumentException:
            raise InstrumentProtocolException(error_code=InstErrorCode.HARDWARE_ERROR,
                                              msg="Could not break from autosample!")

        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 _do_cmd_direct(self, cmd):
        """
        Issue an untranslated command to the instrument. No response is handled as a result of the command.
        Overridden: Use _do_cmd to send commands reliably. Remove if digi-serial interface is ever fixed.

        @param cmd The high level command to issue
        """
        self._do_cmd(cmd)

    def _handler_direct_access_execute_direct(self, data):
        """
        """
        next_state = None
        result = None
        next_agent_state = None

        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return next_state, (next_agent_state, result)

    def _handler_direct_access_stop_direct(self):
        """
        """
        next_state, next_agent_state = self._handler_unknown_discover()
        if next_state == DriverProtocolState.COMMAND:
            next_agent_state = ResourceAgentState.COMMAND

        return next_state, (next_agent_state, None)

    ###################################################################
    # Builders
    ###################################################################
    def _build_default_command(self, *args):
        """
        """
        return " ".join(str(x) for x in args)

    ##################################################################
    # Response parsers
    ##################################################################
    def _parse_set_response(self, response, prompt):
        """Determine if a set was successful or not

        @param response What was sent back from the command that was sent
        @param prompt The prompt that was returned from the device
        """
        if prompt == Prompt.COMMAND:
            return True
        return False

    def _parse_get_response(self, response, prompt):
        """ Parse the response from the instrument for a couple of different
        query responses.

        @param response The response string from the instrument
        @param prompt The prompt received from the instrument
        @return The numerical value of the parameter in the known units
        @raise InstrumentProtocolException When a bad response is encountered
        """
        # should end with the response, an eol, and a prompt
        update_dict = self._param_dict.update_many(response)
        if not update_dict or len(update_dict) > 1:
            log.error("Get response set multiple parameters (%r): expected only 1", update_dict)
            raise InstrumentProtocolException("Invalid response. Bad command?")

        return self._param_dict.get_all()

    def _parse_invalid_response(self, response, prompt):
        """ Parse the response from the instrument for a couple of different
        query responses.

        @param response The response string from the instrument
        @param prompt The prompt received from the instrument
        @return true iff Prompt.INVALID_COMMAND was returned
        """
        # should end with the response, an eoln, and a prompt
        return Prompt.INVALID_COMMAND == prompt

    ###################################################################
    # Helpers
    ###################################################################
    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        Also called when setting parameters during startup and direct access
        In the event an exception is generated dur
        @throws InstrumentParameterException if parameter does not exist or Maxrate is out of range
        @throws InstrumentCommandException if failed to set
        """

        params = args[0]

        self._verify_not_readonly(*args, **kwargs)
        old_config = self._param_dict.get_config()

        exception = None

        for key in params:
            if key not in self._param_dict._param_dict:
                exception = InstrumentParameterException ("Bad parameter: %r" % key)
                break
            val = self._param_dict.format(key, params[key])
            log.debug("KEY = %s VALUE = %s", str(key), str(val))
            if key == Parameter.MAX_RATE and float(params[key]) not in VALID_MAXRATES:
                exception = InstrumentParameterException("Maxrate %s out of range" % val)
                break
            # Check for existance in dict (send only on change)
            if not self._do_cmd_resp(Command.SET, key, val):
                exception = InstrumentCommandException('Error setting: %s = %s' % (key, val))
                break
            self._param_dict.set_value(key, params[key])

        # Get new param dict config. If it differs from the old config,
        # tell driver superclass to publish a config change event.
        new_config = self._param_dict.get_config()
        log.debug("new_config: %s == old_config: %s", new_config, old_config)
        if old_config != new_config:
            self._do_cmd_resp(Command.SAVE, expected_prompt=Prompt.COMMAND)
            log.debug("configuration has changed.  Send driver event")
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Raise any exceptions encountered due to errors setting the parameter(s)
        if exception is not None:
            raise exception

    def _update_params(self, *args, **kwargs):
        """Fetch the parameters from the device, and update the param dict.

        @param args Unused
        @param kwargs Takes timeout value
        """
        old_config = self._param_dict.get_config()
        self.get_config()
        new_config = self._param_dict.get_config()
        if new_config != old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _send_break(self):
        """
        Send break every 0.3 seconds until the Command Console banner is received.
        @throws InstrumentTimeoutException if not Command Console banner not received within 5 seconds.
        """
        self._promptbuf = ""
        self._connection.send(Command.BREAK)
        starttime = time.time()
        resendtime = time.time()
        while True:
            if time.time() > resendtime + 0.3:
                log.debug("Sending break again.")
                self._connection.send(Command.BREAK)
                resendtime = time.time()

            if COMMAND_PATTERN in self._promptbuf:
                break

            if time.time() > starttime + 10:
                raise InstrumentTimeoutException("Break command failing to stop autosample!")

            time.sleep(0.1)

    def _got_chunk(self, chunk, timestamp):
        """
        extract samples from a chunk of data
        @param chunk: bytes to parse into a sample.
        """
        sample = self._extract_sample(self._data_particle_type, self._data_particle_regex, chunk, timestamp) or \
                 self._extract_sample(self._config_particle_type, self._config_particle_regex, chunk, timestamp)
        if not sample:
            raise InstrumentProtocolException(u'unhandled chunk received by _got_chunk: [{0!r:s}]'.format(chunk))
        return sample

    def _confirm_autosample_mode(self):
        """
        Confirm we are in autosample mode.
        This is done by waiting for a sample to come in, and confirming that
        it does or does not.
        @retval True if in autosample mode, False if not
        """
        # timestamp now,
        start_time = self._last_data_timestamp
        # wait a sample period,
        current_maxrate = self._param_dict.get_config()[Parameter.MAX_RATE]
        if current_maxrate is None:
            current_maxrate = 0.125     # During startup, assume the slowest sample rate
        elif current_maxrate <= 0 or current_maxrate > 8:
            current_maxrate = 8         # Effective current maxrate, despite the instrument accepting higher values
        time_between_samples = (1.0 / current_maxrate) + 1
        time.sleep(time_between_samples)
        end_time = self._last_data_timestamp
        log.debug("_confirm_autosample_mode: end_time=%s, start_time=%s" % (end_time, start_time))
        if end_time != start_time:
            log.debug("Confirmed in autosample mode")
            return True
        log.debug("Confirmed NOT in autosample mode")
        return False
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class for SBE16 DOSTA driver.
    """

    particles = [
        DoSampleParticle,
    ]

    def __init__(self, prompts, newline, driver_event):
        """
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The SBE16 newline.
        @param driver_event Driver process event callback.
        """
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # This driver does not process commands, the finite state machine and handlers are stubs
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        handlers = {
            ProtocolState.UNKNOWN: {
                (ProtocolEvent.ENTER, self._handler_state_change()),
                (ProtocolEvent.EXIT, self._handler_pass_through()),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover()),
            },
            ProtocolState.COMMAND: {
                (ProtocolEvent.ENTER, self._handler_state_change()),
                (ProtocolEvent.EXIT, self._handler_pass_through()),
                (ProtocolEvent.GET, self._handler_pass_through()),
            },
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(self.sieve_function)

    @staticmethod
    def sieve_function(raw_data):
        """ The method that splits samples
        Over-ride sieve function to handle additional particles.
        """
        matchers = []
        return_list = []

        matchers.append(DoSampleParticle.regex_compiled())
        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _build_command_dict(self):
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover', timeout=1)

    def _build_param_dict(self):
        pass
        # self._param_dict.add(Parameter.OPTODE,
        #                      r'OPTODE>(.*)</OPTODE',
        #                      lambda match: True if match.group(1) == 'yes' else False,
        #                      self._true_false_to_string,
        #                      type=ParameterDictType.BOOL,
        #                      display_name="Optode Attached",
        #                      description="Enable optode: (true | false)",
        #                      range={'True': True, 'False': False},
        #                      startup_param=True,
        #                      direct_access=True,
        #                      default_value=True,
        #                      visibility=ParameterDictVisibility.IMMUTABLE)
        # self._param_dict.add(Parameter.VOLT1,
        #                      r'ExtVolt1>(.*)</ExtVolt1',
        #                      lambda match: True if match.group(1) == 'yes' else False,
        #                      self._true_false_to_string,
        #                      type=ParameterDictType.BOOL,
        #                      display_name="Volt 1",
        #                      description="Enable external voltage 1: (true | false)",
        #                      range={'True': True, 'False': False},
        #                      startup_param=True,
        #                      direct_access=True,
        #                      default_value=True,
        #                      visibility=ParameterDictVisibility.IMMUTABLE)

    def _got_chunk(self, chunk, timestamp):
        """
        Over-ride sieve function to handle additional particles.
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        if self._extract_sample(DoSampleParticle, DoSampleParticle.regex_compiled(), chunk, timestamp):
            self._sampling = True
            return

    def _build_driver_dict(self):
        """
        Apparently VENDOR_SW_COMPATIBLE is required (TODO - move to the base class)
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    ####################
    # Command Handlers
    ####################
    def _handler_pass_through(self):
        pass

    def _handler_state_change(self):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    # noinspection PyMethodMayBeStatic
    def _handler_unknown_discover(self):
        next_state = ProtocolState.COMMAND
        return next_state, (next_state, None)

    def _handler_command_enter(self):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        next_state, result = self._handler_get(*args, **kwargs)
        # TODO - need to find out why this doesn't match other handler return signatures:
        # TODO   (next_state, (next_state, result)
        return next_state, result
Beispiel #27
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_SCAN, self._handler_command_start_scan),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
            ],
            ProtocolState.SCAN: [
                (ProtocolEvent.ENTER, self._handler_scan_enter),
                (ProtocolEvent.EXIT, self._handler_scan_exit),
                (ProtocolEvent.STOP_SCAN, self._handler_scan_stop_scan),
                (ProtocolEvent.TAKE_SCAN, self._handler_scan_take_scan),
                (ProtocolEvent.TIMEOUT, self._handler_scan_timeout),
                (ProtocolEvent.ERROR, self._handler_scan_error),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.CLEAR, self._handler_error_clear),
                (ProtocolEvent.GET, self._handler_command_get),
            ]
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # 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()

        # Add build handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_build_handler(command, self._generic_build_handler)

        # Add response handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_response_handler(command, functools.partial(self._generic_response_handler, command=command))

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(Protocol.sieve_function)
        self.initialize_scheduler()

        # all calls to do_cmd_resp should expect RESPONSE_REGEX and use TIMEOUT.  Freeze these arguments...
        self._do_cmd_resp = functools.partial(self._do_cmd_resp, response_regex=RESPONSE_REGEX, timeout=TIMEOUT)

        # these variables are used to track scan time and completion status
        # for development and performance data
        self.scan_start_time = 0
        self.in_scan = False

    @staticmethod
    def sieve_function(raw_data):
        """
        This is a placeholder.  The sieve function for the RGA is built dynamically when a scan is started.
        This function must return a list.
        see self._build_sieve_function()
        """
        return []

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        name = 'display_name'
        desc = 'description'
        units = 'units'
        val_desc = 'value_description'

        parameters = {
            Parameter.ID: {
                name: 'RGA ID String',
                desc: '',
            },
            Parameter.EE: {
                name: 'Electron Energy',
                desc: 'The desired electron ionization energy: (25 - 105)',
                units: DriverUnits.ELECTRONVOLT,
                val_desc: 'The desired electron ionization energy in units of eV'
            },
            Parameter.IE: {
                name: 'Ion Energy',
                desc: 'The ion energy: (0:8eV | 1:12eV)',
                val_desc: 'Ion energy level: 0 for Low and 1 for High',
            },
            Parameter.VF: {
                name: 'Focus Plate Voltage',
                desc: 'The focus plate voltage in the ionizer: (0 - 150)',
                val_desc: 'The parameter represents the magnitude of the biasing voltage (negative) in units of volts.',
                units: DriverUnits.VOLT,
            },
            Parameter.NF: {
                name: 'Noise Floor',
                desc: 'Rate and detection limit for ion current measurements: (0 - 7)',
                val_desc: 'The parameter represents the noise-floor level desired. Lower parameter values ' +
                          'correspond to lower baseline noise, better detection limits and increased measurement ' +
                          'times. Please refer to the Electrometer section of the RGA Electronics Control Unit ' +
                          'chapter to obtain detailed information about detection limits and bandwidth values' +
                          'as a function of NF settings.',
            },
            Parameter.SA: {
                name: 'Steps per AMU',
                desc: 'Number of steps executed per amu of analog scan: (10 - 25)',
                val_desc: 'The parameter specifies the number of steps-per-amu.',
                units: DriverUnits.COUNTS,
            },
            Parameter.MI: {
                name: 'Initial Mass',
                desc: 'The initial scan mass: (1 - 200)',
                units: DriverUnits.AMU,
            },
            Parameter.MF: {
                name: 'Final Mass',
                desc: 'The final scan mass: (1 - 200)',
                units: DriverUnits.AMU,
            },
            Parameter.FL: {
                name: 'Electron Emission Current',
                desc: 'Electron emission current level in the ionizer: (0 - 3.5)',
                val_desc: 'The parameter represents the desired electron emission current.',
                units: Prefixes.MILLI + Units.AMPERE
            },
            Parameter.FL_ACTUAL: {
                name: 'Actual Electron Emission Current',
                desc: 'The actual electron emission current level in the ionizer.',
                val_desc: 'The parameter represents the actual electron emission current.',
                units: Prefixes.MILLI + Units.AMPERE
            },
            Parameter.AP: {
                name: 'Analog Scan Points',
                desc: 'The total number of ion currents that will be measured and transmitted ' +
                      'during an analog scan under the current scan conditions.',
                val_desc: 'Total number of ion currents to be transmitted.  Does not include the four extra' +
                          'bytes for total pressure included when performing an analog scan.',
                units: DriverUnits.COUNTS
            },
            Parameter.HV: {
                name: 'High Voltage CDEM',
                desc: 'Electron multiplier high voltage bias setting: (0:disables CDEM, 10 - 2490)',
                val_desc: '0 disables the CDEM, values from 10-2490 enable the CDEM and specify the CDEM bias voltage',
                units: Units.VOLT
            },
            Parameter.ER: {
                name: 'Status Byte',
                desc: 'Bit-mapped value representing any errors detected by the RGA.',
                val_desc: '0 indicates no errors detected.  See the RGA manual if this value is non-zero.',
            },
            Parameter.ERROR_REASON: {
                name: 'RGA Error Reason',
                desc: 'Reason for RGA error state.'
            }
        }

        constraints = ParameterConstraints.dict()
        read_only = [Parameter.ID, Parameter.AP, Parameter.ER, Parameter.FL_ACTUAL, Parameter.ERROR_REASON]
        floats = [Parameter.FL, Parameter.FL_ACTUAL]
        strings = [Parameter.ID, Parameter.ERROR_REASON]

        for param in parameters:
            visibility = ParameterDictVisibility.READ_WRITE
            value_type = ParameterDictType.INT
            formatter = int
            startup = True
            if param in read_only:
                visibility = ParameterDictVisibility.READ_ONLY
                startup = False
            if param in floats:
                value_type = ParameterDictType.FLOAT
                formatter = float
            elif param in strings:
                value_type = ParameterDictType.STRING
                formatter = str

            if param in constraints:
                _type, minimum, maximum = constraints[param]
                parameters[param][val_desc] = '%s %s value from %d - %d' % (parameters[param].get(val_desc, ''),
                                                                            _type, minimum, maximum)

            self._param_dict.add(param, '', None, formatter, type=value_type,
                                 visibility=visibility, startup_param=startup, **parameters[param])

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_SCAN, display_name="Start Scan")
        self._cmd_dict.add(Capability.STOP_SCAN, display_name="Stop Scan")
        self._cmd_dict.add(Capability.CLEAR, display_name="Clear Error State")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _got_chunk(self, chunk, ts):
        """
        The base class got_data has gotten a chunk from the chunker.
        We only generate sample particles and they cannot be verified (beyond size, which is done in the chunker).
        Just create a particle, reset the scheduler and start the next scan.
        @param chunk: data to process
        @param ts: timestamp
        """
        elapsed = time.time() - self.scan_start_time
        self.in_scan = False
        log.debug('_got_chunk: Received complete scan.  AP: %d NF: %d SIZE: %d ET: %d secs',
                  self._param_dict.get(Parameter.AP),
                  self._param_dict.get(Parameter.NF),
                  len(chunk),
                  elapsed)
        self._driver_event(DriverAsyncEvent.SAMPLE, RGASampleParticle(chunk, port_timestamp=ts).generate())
        # Reset the scheduler and initiate the next scan if we are in the scan state
        if self.get_current_state() == ProtocolState.SCAN:
            self._build_scheduler()
            self._async_raise_fsm_event(ProtocolEvent.TAKE_SCAN)

    def _generic_response_handler(self, resp, prompt, command=None):
        """
        Generic response handler.  Shove the results into the param dict.
        The associated command should be frozen when the response handler is registered using functools.partial
        @param resp: command response
        @param prompt: not used, required to match signature
        @param command: command which generated response
        @return: response
        """
        parameter = getattr(Parameter, command, None)
        log.debug('_generic_response_handler: command: %s parameter: %s resp: %s', command, parameter, resp)
        if parameter in self._param_dict.get_keys():
            if parameter == Parameter.FL:
                parameter = Parameter.FL_ACTUAL
            try:
                self._param_dict.set_value(parameter, self._param_dict.format(parameter, resp))
            except ValueError:
                # bad data?  Don't set the value, but keep the driver moving forward
                # verify the data if necessary downstream.
                pass
        return resp

    def _generic_build_handler(self, command, *args, **kwargs):
        """
        Generic build handler.  If a value is passed, then this is a set, otherwise it's a query...
        @param command: command to build
        @param args: arglist which may contain a value
        @return: command string
        """
        if len(args) == 1:
            # this is a set action
            value = args[0]
            return self._build_rga_set(command, value) + NEWLINE
        # this is a query
        return self._build_rga_query(command) + NEWLINE

    def _build_rga_set(self, command, value):
        """
        Build a set command
        @param command: command to build
        @param value: value to set
        @return: command string
        """
        return command + str(value)

    def _build_rga_query(self, command):
        """
        Build a query command
        @param command: command to build
        @return: command string
        """
        return command + '?'

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events: list of events to be filtered
        @return: list of events which are in capability
        """
        return [x for x in events if Capability.has(x)]

    def _wakeup(self, timeout, delay=1):
        """
        Wakeup not required for this instrument
        """

    def _build_scheduler(self):
        """
        Remove any previously scheduled event, then generate an absolute trigger to schedule the next
        scan in case we lose some data and the next scan isn't triggered by got_chunk.
        """
        try:
            self._remove_scheduler(ScheduledJob.TAKE_SCAN)
            log.debug('Successfully removed existing scheduled event TAKE_SCAN.')
        except KeyError as ke:
            log.debug('KeyError: %s', ke)

        # this formula was derived from testing, should yield a slightly higher time than the actual
        # time required to collect a single scan.
        delay = self._param_dict.get(Parameter.AP) / 9 / self._param_dict.get(Parameter.NF) + 5

        if delay > 0:
            dt = datetime.datetime.now() + datetime.timedelta(seconds=delay)

            job_name = ScheduledJob.TAKE_SCAN
            config = {
                DriverConfigKey.SCHEDULER: {
                    job_name: {
                        DriverSchedulerConfigKey.TRIGGER: {
                            DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.ABSOLUTE,
                            DriverSchedulerConfigKey.DATE: dt
                        },
                    }
                }
            }

            self.set_init_params(config)
            self._add_scheduler_event(ScheduledJob.TAKE_SCAN, ProtocolEvent.TIMEOUT)

    def _update_params(self, *args, **kwargs):
        """
        Parameters are NOT set in the instrument by this method, as the RGA is configured anytime
        a scan is started, as it may have been powered off since the last time we saw it.
        """

    def _set_params(self, *args, **kwargs):
        """
        Set parameters, raise a CONFIG_CHANGE event if necessary.
        @throws InstrumentParameterException
        """
        self._verify_not_readonly(*args, **kwargs)
        params_to_set = args[0]
        old_config = self._param_dict.get_all()

        # check if in range
        constraints = ParameterConstraints.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params_to_set.iteritems():
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                try:
                    value = var_type(val)
                except ValueError:
                    raise exceptions.InstrumentParameterException(
                        'Unable to verify type - parameter: %s value: %s' % (key, val))
                if val < minimum or val > maximum:
                    raise exceptions.InstrumentParameterException(
                        'Value out of range - parameter: %s value: %s min: %s max: %s' %
                        (key, val, minimum, maximum))

        # all constraints met or no constraints exist, set the values
        for key, val in params_to_set.iteritems():
            if key in old_config:
                self._param_dict.set_value(key, val)
            else:
                raise exceptions.InstrumentParameterException(
                    'Attempted to set unknown parameter: %s value: %s' % (key, val))
        new_config = self._param_dict.get_all()

        # If we changed anything, raise a CONFIG_CHANGE event
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _check_error_byte(self, error_string):
        """
        Check the error byte as returned by some commands
        @param error_string: byte to be checked for errors
        @throws InstrumentStateException
        """
        # trim, just in case we received some garbage with our response...
        if len(error_string) > 1:
            error_string = error_string[-1]
        if int(error_string):
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
            error = 'RGA Error byte set: %s' % error_string
            self._param_dict.set_value(Parameter.ERROR_REASON, error)
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
            raise exceptions.InstrumentStateException(error)

    def _set_instrument_parameter(self, command):
        """
        Set a parameter on the instrument.
        We will attempt up to MAX_SET_RETRIES to set the value correctly, according to the following sequence:
        1. send set command
        2. verify error byte, if returned (per Responses)
        3. send query command
        4. verify returned value equals the set value (within tolerance)
        @throws InstrumentParameterException
        """
        response_type = getattr(Responses, command)
        parameter = getattr(Parameter, command)

        # store the configured setting
        old_value = self._param_dict.format(parameter)

        if old_value is None:
            raise exceptions.InstrumentParameterException('Missing required instrument parameter: %s' % parameter)

        log.debug('response_type: %s parameter: %s command: %s', response_type, getattr(Parameter, command), command)
        # attempt to set the value up to MAX_SET_RETRIES times
        for x in xrange(MAX_RETRIES):
            if response_type == STATUS:
                resp = self._do_cmd_resp(command, old_value)
                self._check_error_byte(resp)
            else:
                self._do_cmd_no_resp(command, old_value)

            # query the value from the instrument to load the parameter dictionary
            self._do_cmd_resp(command)

            # if values match, we were successful, return.
            difference = abs(self._param_dict.format(parameter) - old_value)
            if difference < CLOSE_ENOUGH:
                return
            log.error('Set attempt failed. Parameter: %s Set value: %s Returned value: %s difference: %.2f',
                      parameter, old_value, self._param_dict.get(parameter), difference)

        # configuring the RGA failed, restore the setting from our configuration and raise an exception
        self._param_dict.set_value(parameter, old_value)
        raise exceptions.InstrumentParameterException('Unable to set instrument parameter: %s, attempted %d times' %
                                                      (parameter, MAX_RETRIES))

    def _build_sieve_function(self):
        """
        Build a sieve function based on the expected data size.  Replace the previous sieve function in
        the chunker.  This should happen during the configuration phase.
        """
        num_points = int(self._param_dict.get(Parameter.AP))
        match_string = r'(?<=%s)(.{%d})' % (SCAN_START_SENTINEL, (num_points + 1) * 4)
        matcher = re.compile(match_string, re.DOTALL)

        def my_sieve(raw_data):
            return_list = []

            log.debug('SIEVE: pattern=%r, raw_data_len=%d', matcher.pattern, len(raw_data))
            # do not descend into this loop unless we are at log level trace...
            if log.isEnabledFor('trace'):
                temp = raw_data[:]
                while temp:
                    log.trace('SIEVE: raw_data: %s', temp[:32].encode('hex'))
                    if len(temp) > 32:
                        temp = temp[32:]
                    else:
                        temp = ''

            for match in matcher.finditer(raw_data):
                # if sentinel value present in this slice it is invalid
                if not SCAN_START_SENTINEL in raw_data[match.start():match.end()]:
                    return_list.append((match.start(), match.end()))

            return return_list

        self._chunker.sieve = my_sieve

    def _verify_filament(self):
        """
        Ensure the filament is on and the current is within tolerance
        @throws InstrumentProtocolException
        """
        self._do_cmd_resp(InstrumentCommand.FILAMENT_EMISSION)
        filament_difference = abs(1 - self._param_dict.get(Parameter.FL_ACTUAL))
        if filament_difference > CLOSE_ENOUGH:
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
            error = 'Filament power not withing tolerance (%.2f): %.2f' % (CLOSE_ENOUGH, filament_difference)
            self._param_dict.set_value(Parameter.ERROR_REASON, error)
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
            raise exceptions.InstrumentProtocolException(error)

    def _stop_instrument(self):
        """
        Stop any running scan, flush the output buffer, turn off the filament and CDEM.
        Update the parameter dictionary for FL.
        """
        try:
            self._remove_scheduler(ScheduledJob.TAKE_SCAN)
            log.debug('Successfully removed existing scheduled event TAKE_SCAN.')
        except KeyError as ke:
            log.debug('KeyError: %s', ke)

        self._do_cmd_resp(InstrumentCommand.INITIALIZE, 0)
        self._do_cmd_resp(InstrumentCommand.INITIALIZE, 2)
        self._do_cmd_resp(InstrumentCommand.FILAMENT_EMISSION)
        self.in_scan = False

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @return (next_state, result)
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameter
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        """
        self._set_params(*args, **kwargs)
        return None, None

    def _handler_command_start_direct(self):
        """
        Start direct access
        @return next_state, (next_agent_state, None)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS, None)

    def _handler_command_start_scan(self):
        """
        Start a scan
        @return next_state, (next_agent_state, None)
        """
        return ProtocolState.SCAN, (ResourceAgentState.STREAMING, 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_execute_direct(self, data):
        """
        Forward direct access commands to the instrument.
        @return next_state, (next_agent_state, None)
        """
        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)
        return None, (None, None)

    def _handler_direct_access_stop_direct(self):
        """
        Stop direct access, return to COMMAND.
        @return next_state, (next_agent_state, None)
        """
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Scan handlers
    ########################################################################

    def _handler_scan_enter(self, *args, **kwargs):
        """
        Enter the scan state.  Configure the RGA, start the first scan and the scheduler.
        @throws InstrumentTimeoutException
        """
        for attempt in range(1, MAX_RETRIES+1):
            try:
                self._handler_scan_configure_rga()
                self._async_raise_fsm_event(ProtocolEvent.TAKE_SCAN)
                self._build_scheduler()
                self._driver_event(DriverAsyncEvent.STATE_CHANGE)
                return
            except exceptions.InstrumentTimeoutException:
                log.error('Failed to configure the RGA - attempt %d', attempt)
        self._async_raise_fsm_event(ProtocolEvent.ERROR)
        error = 'Failed to configure RGA and start scanning.'
        self._param_dict.set_value(Parameter.ERROR_REASON, error)
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        raise exceptions.InstrumentTimeoutException(error)

    def _handler_scan_exit(self, *args, **kwargs):
        """
        Exit scan.  Delete the scheduler.
        """
        try:
            self._remove_scheduler(ScheduledJob.TAKE_SCAN)
        except KeyError:
            log.error("_remove_scheduler could not find: %s", ScheduledJob.TAKE_SCAN)

    def _handler_scan_configure_rga(self):
        """
        Send the appropriate configuration to the RGA and update the chunker sieve function for the
        correct data length.
        """
        # initialize the connection
        self._do_cmd_resp(InstrumentCommand.INITIALIZE, 0)

        # set these
        set_commands = [
            (InstrumentCommand.ELECTRON_ENERGY, Parameter.EE),
            (InstrumentCommand.ION_ENERGY, Parameter.IE),
            (InstrumentCommand.FOCUS_VOLTAGE, Parameter.VF),
            (InstrumentCommand.NOISE_FLOOR, Parameter.NF),
            (InstrumentCommand.STEPS_PER_AMU, Parameter.SA),
            (InstrumentCommand.INITIAL_MASS, Parameter.MI),
            (InstrumentCommand.FINAL_MASS, Parameter.MF),
        ]

        for command, parameter in set_commands:
            self._set_instrument_parameter(command)

        # turn on the filament
        self._set_instrument_parameter(InstrumentCommand.FILAMENT_EMISSION)

        # query the read only items
        for command in [InstrumentCommand.READINGS_PER_SCAN, InstrumentCommand.FILAMENT_EMISSION,
                        InstrumentCommand.ID, InstrumentCommand.CHECK_ERRORS]:
            self._do_cmd_resp(command)

        # publish the config as a status particle
        pd = self._param_dict.get_all()
        log.debug('parameter dictionary: %r', pd)
        ts = ntplib.system_to_ntp_time(time.time())
        self._driver_event(DriverAsyncEvent.SAMPLE, RGAStatusParticle(pd, port_timestamp=ts).generate())

        # replace the sieve function
        self._build_sieve_function()

    def _handler_scan_take_scan(self, *args, **kwargs):
        """
        place a sentinel value in the chunker, then perform one analog scan from the RGA
        @return next_state, (next_agent_state, None)
        """
        # empty the chunker
        self._chunker.clean()
        # place sentinel value in chunker
        self._chunker.add_chunk(SCAN_START_SENTINEL, ntplib.system_to_ntp_time(time.time()))
        self.scan_start_time = time.time()
        if self.in_scan:
            log.error('FAILED scan detected, in_scan sentinel set to TRUE')
        self.in_scan = True
        self._do_cmd_no_resp(InstrumentCommand.ANALOG_SCAN, 1)
        return None, (None, None)

    def _handler_scan_timeout(self, *args, **kwargs):
        """
        Handle scan timeout
        @return next_state, (next_agent_state, None)
        """
        # timeout, clear the instrument buffers
        self._do_cmd_resp(InstrumentCommand.INITIALIZE, 0)
        # verify the filament is still on
        self._verify_filament()
        return self._handler_scan_take_scan()

    def _handler_scan_stop_scan(self, *args, **kwargs):
        """
        Stop scanning, go to COMMAND.
        @return next_state, (next_agent_state, None)
        """
        self._stop_instrument()
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    def _handler_scan_error(self, *args, **kwargs):
        """
        Stop scanning, go to ERROR.
        @return next_state, (next_agent_state, None)
        """
        self._stop_instrument()
        return ProtocolState.ERROR, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Error handlers
    ########################################################################

    def _handler_error_clear(self):
        """
        Leave the error state, return to COMMAND.
        @return next_state, (next_agent_state, None)
        """
        self._param_dict.set_value(Parameter.ERROR_REASON, '')
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Generic handlers
    ########################################################################

    def _handler_generic_enter(self):
        """
        Generic method to handle entering state.
        """
        if self.get_current_state() != ProtocolState.UNKNOWN:
            self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self):
        """
Beispiel #28
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = METALOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.

        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START1, self._handler_command_start1),
                (ProtocolEvent.NAFREG, self._handler_command_nafreg),
                (ProtocolEvent.IONREG, self._handler_command_ionreg),
                (ProtocolEvent.POWEROFF, self._handler_command_poweroff),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.START1: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START1_COMPLETE, self._handler_start1_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.WAITING_TURBO: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.START2, self._handler_waiting_turbo_start2),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.START2: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START2_COMPLETE, self._handler_start2_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.WAITING_RGA: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.SAMPLE, self._handler_waiting_rga_sample),
                (ProtocolEvent.CALIBRATE, self._handler_waiting_rga_cal),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.SAMPLE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.SAMPLE_COMPLETE, self._handler_sample_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.CALIBRATE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.CALIBRATE_COMPLETE, self._handler_cal_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.STOPPING: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.REGEN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_stop),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_error_standby),
                (ProtocolEvent.CLEAR, self._handler_stop),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # response handlers
        for command in InstrumentCommand.list():
            self._add_response_handler(command, functools.partial(self._generic_response_handler, command=command))

        # 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()

        # Add build handlers for device commands.
        for command in InstrumentCommand.list():
            if command == InstrumentCommand.SET_TELEGRAM_INTERVAL:
                self._add_build_handler(command, self._build_telegram_interval_command)
            elif command == InstrumentCommand.SAMPLE:
                self._add_build_handler(command, self._build_sample_command)
            elif command == InstrumentCommand.SET_MINUTE:
                self._add_build_handler(command, self._build_set_minute_command)
            else:
                self._add_build_handler(command, self._build_simple_command)

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(Protocol.sieve_function)

        self.resetting = False

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        @param raw_data - data to be searched
        """
        matchers = []
        return_list = []

        matchers.append(McuDataParticle.regex_compiled())
        matchers.append(re.compile(r'(M .*?)(?=\r)'))
        matchers.append(re.compile(r'(E\d{3}.*?)(?=\r)'))

        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _build_param_dict(self):
        """
        Build the parameter dictionary
        """
        self._param_dict.add(Parameter.TELEGRAM_INTERVAL,
                             '',
                             None,
                             None,
                             type=ParameterDictType.INT,
                             startup_param=True,
                             display_name='Data Telegram Interval in Sample',
                             units=Prefixes.MILLI + Units.SECOND,
                             description='The interval between successive MCU data telegrams' +
                                         ' while in the SAMPLE/CAL state: (1 - 30000)')
        self._param_dict.add(Parameter.SAMPLE_TIME,
                             '',
                             None,
                             None,
                             type=ParameterDictType.INT,
                             startup_param=True,
                             display_name='Sample Cycle Time',
                             units=Units.MINUTE,
                             description='The length of each portion of the sample cycle: (1 - 99)')
        self._param_dict.add(Parameter.ONE_MINUTE,
                             '',
                             None,
                             None,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             type=ParameterDictType.INT,
                             startup_param=True,
                             default_value=60000,
                             display_name='Length of One Minute',
                             units=Prefixes.MILLI + Units.SECOND,
                             description='MCU timing constant representing the number of seconds per minute: (1 - 99999)')
        self._param_dict.add(Parameter.ERROR_REASON,
                             '',
                             None,
                             None,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             type=ParameterDictType.STRING,
                             value='',
                             display_name='Reason for Error State',
                             description='MCU reason for error state.')

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START1, display_name="Execute ASTART1")
        self._cmd_dict.add(Capability.START2, display_name="Execute ASTART2")
        self._cmd_dict.add(Capability.SAMPLE, display_name="Execute ASAMPLEXX")
        self._cmd_dict.add(Capability.CALIBRATE, display_name="Execute ACAL9")
        self._cmd_dict.add(Capability.NAFREG, display_name="Execute U ANAFREG3")
        self._cmd_dict.add(Capability.IONREG, display_name="Execute U AIONREG3")
        self._cmd_dict.add(Capability.STANDBY, display_name="Execute U ASTANDBY")
        self._cmd_dict.add(Capability.CLEAR, display_name="Clear Error State")
        self._cmd_dict.add(Capability.POWEROFF, display_name="Execute U APOWEROFF")

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_telegram_interval_command(self, *args, **kwargs):
        """
        Build the telegram interval command using the TELEGRAM_INTERVAL parameter
        """
        return '%s%08d%s' % (InstrumentCommand.SET_TELEGRAM_INTERVAL,
                             int(self._param_dict.get(Parameter.TELEGRAM_INTERVAL)),
                             NEWLINE)

    def _build_set_minute_command(self, *args, **kwargs):
        """
        Build the SETMINUTE command
        """
        return '%s%05d%s' % (InstrumentCommand.SET_MINUTE,
                             int(self._param_dict.get(Parameter.ONE_MINUTE)),
                             NEWLINE)

    def _build_sample_command(self, *args, **kwargs):
        """
        Build the SAMPLE command
        """
        return '%s%02d%s' % (InstrumentCommand.SAMPLE,
                             int(self._param_dict.get(Parameter.SAMPLE_TIME)),
                             NEWLINE)

    def _got_chunk(self, chunk, ts):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and regexes.

        Raise specific events on receipt of chunks.  This allows the driver to react asynchronously.

        @param chunk - data to be converted to a particle
        @param ts - timestamp
        """
        event = None
        exception = None
        sample = self._extract_sample(McuDataParticle, McuDataParticle.regex_compiled(), chunk, ts)
        if sample:
            return

        # we don't want to act on any responses in direct access or command mode
        # so just return here if that's the case...
        current_state = self.get_current_state()
        if current_state in [ProtocolState.DIRECT_ACCESS, ProtocolState.COMMAND]:
            return

        # These responses (may) come from the instrument asynchronously, so they are handled
        # here rather than in a response handler.
        ignored = [Prompt.OK, Prompt.BEAT, Prompt.STANDBY]
        if chunk in ignored:
            pass
        elif chunk == Prompt.START1:
            event = ProtocolEvent.START1_COMPLETE
        elif chunk == Prompt.START2:
            event = ProtocolEvent.START2_COMPLETE
        elif chunk == Prompt.SAMPLE_FINISHED:
            event = ProtocolEvent.SAMPLE_COMPLETE
        elif chunk == Prompt.CAL_FINISHED:
            event = ProtocolEvent.CALIBRATE_COMPLETE
        elif chunk == Prompt.IONREG_FINISHED:
            event = ProtocolEvent.STANDBY
        elif chunk == Prompt.NAFREG_FINISHED:
            event = ProtocolEvent.STANDBY
        elif chunk == Prompt.ERROR:
            event = ProtocolEvent.ERROR
            self._param_dict.set_value(Parameter.ERROR_REASON, 'Error prompt received from instrument.')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        elif chunk == Prompt.ONLINE:
            if not self.resetting:
                # This is an unexpected reset, ignore if we are in command or error
                if current_state == ProtocolState.ERROR:
                    event = ProtocolEvent.ERROR
                    self._param_dict.set_value(Parameter.ERROR_REASON, 'MCU reset during sequence.')
                    self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        elif chunk in [Prompt.NAFTEMP_NOT_ACHIEVED, Prompt.IONTEMP_NOT_ACHIEVED]:
            # regeneration temperature not achieved, move to COMMAND and raise an exception
            event = ProtocolEvent.STANDBY
            exception = InstrumentProtocolException('Failed to achieve regen temperature')
        else:
            log.error('Unhandled chunk: %r in state: %s', chunk, current_state)
            exception = InstrumentProtocolException('Unhandled chunk: %r in state: %s' % (chunk, current_state))

        if event is not None:
            self._async_raise_fsm_event(event)
        if exception:
            self._driver_event(DriverAsyncEvent.ERROR, exception)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events - events to be filtered
        @return list of events which are also capabilities
        """
        return [x for x in events if Capability.has(x)]

    def _wakeup(self, *args, **kwargs):
        """
        Not needed, the MCU never sleeps...
        """

    def _generic_response_handler(self, result, prompt, command=None):
        """
        Generic response handler to pass results through unmodified.
        @param result - result
        @param prompt - prompt
        @command - Command which generated the result
        @return result
        """
        return result

    def _set_params(self, *args, **kwargs):
        """
        This instrument has no params
        @throws InstrumentParameterException
        """
        self._verify_not_readonly(*args, **kwargs)
        params_to_set = args[0]
        startup = False
        if len(args) > 1:
            startup = args[1]
        old_config = self._param_dict.get_all()

        # check if in range
        constraints = ParameterConstraint.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params_to_set.iteritems():
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                try:
                    value = var_type(val)
                except ValueError:
                    raise InstrumentParameterException(
                        'Unable to verify type - parameter: %s value: %s' % (key, val))
                if val < minimum or val > maximum:
                    raise InstrumentParameterException(
                        'Value out of range - parameter: %s value: %s min: %s max: %s' %
                        (key, val, minimum, maximum))

        # all constraints met or no constraints exist, set the values
        for key, val in params_to_set.iteritems():
            if key in old_config:
                self._param_dict.set_value(key, val)
            else:
                raise InstrumentParameterException(
                    'Attempted to set unknown parameter: %s value: %s' % (key, val))
        new_config = self._param_dict.get_all()

        # If we changed anything, raise a CONFIG_CHANGE event
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _reset_mcu(self):
        """
        Reset the MCU via the watchdog timer
        """
        try:
            self.resetting = True
            # set the watchdog timer
            self._do_cmd_resp(InstrumentCommand.SET_WATCHDOG, expected_prompt=Prompt.OK, timeout=60)
            # try to put the MCU in standby, if successful watchdog will reset MCU
            result = self._do_cmd_resp(InstrumentCommand.STANDBY,
                                       expected_prompt=[Prompt.ONLINE, Prompt.IN_SEQUENCE], timeout=60)
            # MCU was in sequence, abort it and then go standby to reset MCU
            if result == Prompt.IN_SEQUENCE:
                self._do_cmd_resp(InstrumentCommand.ABORT, expected_prompt=Prompt.ABORTED, timeout=60)
                self._do_cmd_resp(InstrumentCommand.STANDBY, expected_prompt=Prompt.ONLINE, timeout=60)
            # MCU expects a BEAT after reset, send it
            self._do_cmd_resp(InstrumentCommand.BEAT, expected_prompt=Prompt.BEAT)
            # set the MINUTE value
            self._do_cmd_resp(InstrumentCommand.SET_MINUTE, expected_prompt=Prompt.SET_MINUTE)
            # This should actually put us in standby
            self._do_cmd_resp(InstrumentCommand.STANDBY, expected_prompt=Prompt.STANDBY, timeout=60)
        finally:
            self.resetting = False

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @return_value (next_state, result)
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.  Break out of any currently running sequence and return the MCU to STANDBY
        """
        self._init_params()

        try:
            self._reset_mcu()
        except InstrumentTimeoutException:
            # something else is wrong, pass the buck to the operator
            self._param_dict.set_value(Parameter.ERROR_REASON, 'Timeout communicating with instrument.')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
            self._async_raise_fsm_event(ProtocolEvent.ERROR)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        This driver has no parameters, return an empty dict.
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        """
        self._set_params(*args, **kwargs)
        return None, None

    def _handler_command_start_direct(self):
        """
        Start direct access
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS, None)

    def _handler_command_start1(self):
        """
        Send the start1 command and move to the start1 state
        @return next_state, (next_agent_state, result)
        """
        self._reset_mcu()
        return ProtocolState.START1, (ResourceAgentState.BUSY, self._do_cmd_resp(InstrumentCommand.START1))

    def _handler_command_nafreg(self):
        """
        Send the nafreg command and move to the nafreg state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.REGEN, (ResourceAgentState.BUSY, self._do_cmd_resp(InstrumentCommand.NAFREG))

    def _handler_command_ionreg(self):
        """
        Send the ionreg command and move to the ionreg state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.REGEN, (ResourceAgentState.BUSY, self._do_cmd_resp(InstrumentCommand.IONREG))

    def _handler_command_poweroff(self):
        """
        Send the ionreg command and move to the ionreg state
        @return next_state, (next_agent_state, result)
        """
        return None, (None, self._do_cmd_resp(InstrumentCommand.POWEROFF))

    ########################################################################
    # START1 handlers.
    ########################################################################

    def _handler_start1_complete(self):
        """
        Start1 sequence complete, move to waiting_turbo
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.WAITING_TURBO, (ResourceAgentState.IDLE, None)

    ########################################################################
    # WAITING_TURBO handlers.
    ########################################################################

    def _handler_waiting_turbo_start2(self):
        """
        Turbo is at speed, send start2 and move to start2 state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.START2, (ResourceAgentState.BUSY, self._do_cmd_resp(InstrumentCommand.START2))

    ########################################################################
    # START2 handlers.
    ########################################################################

    def _handler_start2_complete(self):
        """
        Start2 complete, move to waiting_rga state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.WAITING_RGA, (ResourceAgentState.BUSY, None)

    ########################################################################
    # WAITING_RGA handlers.
    ########################################################################

    def _handler_waiting_rga_sample(self):
        """
        RGA configuration/startup complete, send start sample and move to sample state
        @return next_state, (next_agent_state, result)
        """
        result = self._do_cmd_resp(InstrumentCommand.SAMPLE)
        self._do_cmd_resp(InstrumentCommand.SET_TELEGRAM_INTERVAL)
        return ProtocolState.SAMPLE, (ResourceAgentState.BUSY, result)

    def _handler_waiting_rga_cal(self):
        """
        RGA configuration/startup complete, send start cal and move to cal state
        @return next_state, (next_agent_state, result)
        """
        result = self._do_cmd_resp(InstrumentCommand.CAL)
        self._do_cmd_resp(InstrumentCommand.SET_TELEGRAM_INTERVAL)
        return ProtocolState.CALIBRATE, (ResourceAgentState.BUSY, result)

    ########################################################################
    # SAMPLE handlers.
    ########################################################################

    def _handler_sample_complete(self):
        """
        Sample complete, move to the stopping state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.STOPPING, (ResourceAgentState.BUSY, None)

    ########################################################################
    # CALIBRATE handlers.
    ########################################################################

    def _handler_cal_complete(self):
        """
        Cal complete, move to the stopping state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.STOPPING, (ResourceAgentState.BUSY, None)

    ########################################################################
    # ERROR handler. Handle in all states.
    ########################################################################

    def _handler_error(self):
        """
        Error detected, move to error state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.ERROR, (ResourceAgentState.BUSY, None)

    def _handler_stop(self):
        """
        Return to COMMAND
        """
        if self._param_dict.get(Parameter.ERROR_REASON):
            self._param_dict.set_value(Parameter.ERROR_REASON, '')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    def _handler_error_standby(self):
        """
        Move instrument to STANDBY, stay in error state
        """
        self._reset_mcu()

    ########################################################################
    # GENERIC handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler
        """

    ########################################################################
    # 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):
        """
        Pass direct access commands through to the instrument
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)
        return None, (None, 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
Beispiel #30
0
class Protocol(Pco2wProtocol):
    """
    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.
        """

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Construct protocol superclass.
        Pco2wProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # build the chunker
        self._chunker = StringChunker(Protocol.sieve_function)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """

        return [x for x in events if Capability.has(x)]

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        :param raw_data: data to filter
        """

        return_list = []

        sieve_matchers = [
            SAMI_REGULAR_STATUS_REGEX_MATCHER, PCO2W_SAMPLE_REGEX_MATCHER,
            PCO2WA_CONFIGURATION_REGEX_MATCHER, SAMI_ERROR_REGEX_MATCHER
        ]

        for matcher in sieve_matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker. Pass it to
        extract_sample with the appropriate particle objects and REGEXes.
        """

        if any([
                self._extract_sample(SamiRegularStatusDataParticle,
                                     SAMI_REGULAR_STATUS_REGEX_MATCHER, chunk,
                                     timestamp),
                self._extract_sample(Pco2waConfigurationDataParticle,
                                     PCO2WA_CONFIGURATION_REGEX_MATCHER, chunk,
                                     timestamp)
        ]):
            return

        sample = self._extract_sample(Pco2wSamiSampleDataParticle,
                                      PCO2W_SAMPLE_REGEX_MATCHER_NORMAL, chunk,
                                      timestamp)
        if sample is None:
            sample = self._extract_sample(
                Pco2wSamiSampleCalibrationDataParticle,
                PCO2W_SAMPLE_REGEX_MATCHER_CAL, chunk, timestamp)

        log.debug('Protocol._got_chunk(): get_current_state() == %s',
                  self.get_current_state())

        if sample:
            self._verify_checksum(chunk, PCO2W_SAMPLE_REGEX_MATCHER)

    ########################################################################
    # Build Command, Driver and Parameter dictionaries
    ########################################################################

    def _build_param_dict(self):
        """
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        Pco2wProtocol._build_param_dict(self)

        ### example configuration string
        # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \
        #                       '000000000D000000000D000000000D07' + \
        #                       '1020FF54181C010038' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE
        #
        ###

        configuration_string_regex = self._get_configuration_string_regex()

        # Changed from 0x0A to 0x02 to indicate there is no external device, update IOS to indicate this is 0x02
        self._param_dict.add(Parameter.MODE_BITS,
                             configuration_string_regex,
                             lambda match: int(match.group(4), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x02,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Mode Bits')

        ## Changed from 0x000E10 to 0x000000 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL,
                             configuration_string_regex,
                             lambda match: int(match.group(8), 16),
                             lambda x: self._int_to_hexstring(x, 6),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x000000,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Sample Interval')

        ## Changed from 0x01 to 0x00 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION,
                             configuration_string_regex,
                             lambda match: int(match.group(9), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x00,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Driver Version')

        ## Changed from 0x0B to 0x00 to indicate there is not external device
        self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER,
                             configuration_string_regex,
                             lambda match: int(match.group(10), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x00,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Parameter Pointer')

    ########################################################################
    # Overridden base class methods
    ########################################################################

    def _get_specific_configuration_string_parameters(self):
        """
        Overridden by device specific subclasses.
        """

        # An ordered list of parameters, can not use unordered dict
        # PCO2W driver extends the base class (SamiParameter)
        parameter_list = [
            Parameter.START_TIME_FROM_LAUNCH, Parameter.STOP_TIME_FROM_START,
            Parameter.MODE_BITS, Parameter.SAMI_SAMPLE_INTERVAL,
            Parameter.SAMI_DRIVER_VERSION, Parameter.SAMI_PARAMS_POINTER,
            Parameter.DEVICE1_SAMPLE_INTERVAL,
            Parameter.DEVICE1_DRIVER_VERSION, Parameter.DEVICE1_PARAMS_POINTER,
            Parameter.DEVICE2_SAMPLE_INTERVAL,
            Parameter.DEVICE2_DRIVER_VERSION, Parameter.DEVICE2_PARAMS_POINTER,
            Parameter.DEVICE3_SAMPLE_INTERVAL,
            Parameter.DEVICE3_DRIVER_VERSION, Parameter.DEVICE3_PARAMS_POINTER,
            Parameter.PRESTART_SAMPLE_INTERVAL,
            Parameter.PRESTART_DRIVER_VERSION,
            Parameter.PRESTART_PARAMS_POINTER, Parameter.GLOBAL_CONFIGURATION,
            Parameter.PUMP_PULSE, Parameter.PUMP_DURATION,
            Parameter.SAMPLES_PER_MEASUREMENT, Parameter.CYCLES_BETWEEN_BLANKS,
            Parameter.NUMBER_REAGENT_CYCLES, Parameter.NUMBER_BLANK_CYCLES,
            Parameter.FLUSH_PUMP_INTERVAL, Parameter.BIT_SWITCHES,
            Parameter.NUMBER_EXTRA_PUMP_CYCLES
        ]

        return parameter_list

    def _get_configuration_string_regex(self):
        """
        Get configuration string regex.
        @retval configuration string regex.
        """
        return PCO2WA_CONFIGURATION_REGEX

    def _get_configuration_string_regex_matcher(self):
        """
        Get config string regex matcher.
        @retval configuration string regex matcher
        """
        return PCO2WA_CONFIGURATION_REGEX_MATCHER
Beispiel #31
0
class Protocol(InstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, driver_event):
        """
        Protocol constructor.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        InstrumentProtocol.__init__(self, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT,
                 self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_AUTOSAMPLE,
                 self._handler_command_start_autosample),
                (ProtocolEvent.ACQUIRE_SAMPLE,
                 self._handler_command_start_poll),
                (ProtocolEvent.CALIBRATE,
                 self._handler_command_start_calibrate),
                (ProtocolEvent.START_NAFION,
                 self._handler_command_start_nafion_regen),
                (ProtocolEvent.START_ION,
                 self._handler_command_start_ion_regen),
                (ProtocolEvent.ERROR, self._handler_error),
                (ProtocolEvent.POWEROFF, self._handler_command_poweroff),
                (ProtocolEvent.START_MANUAL,
                 self._handler_command_start_manual),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_SAMPLE,
                 self._handler_autosample_acquire_sample),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.POLL: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.CLEAR, self._handler_error_clear),
            ],
            ProtocolState.CALIBRATE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.REGEN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_REGEN, self._handler_stop_regen),
                (ProtocolEvent.REGEN_COMPLETE, self._handler_regen_complete),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_direct_access_exit),
                (ProtocolEvent.STOP_DIRECT,
                 self._handler_direct_access_stop_direct),
                (ProtocolEvent.EXECUTE_DIRECT,
                 self._handler_direct_access_execute_direct),
            ],
            ProtocolState.MANUAL_OVERRIDE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_MANUAL,
                 self._handler_manual_override_stop),
                (ProtocolEvent.GET_SLAVE_STATES,
                 self._handler_manual_get_slave_states),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # 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()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._slave_protocols = {}
        self.initialize_scheduler()

    def _add_manual_override_handlers(self):
        for slave in self._slave_protocols:
            for event in self._slave_protocols[slave]._cmd_dict._cmd_dict:
                self._protocol_fsm.add_handler(
                    ProtocolState.MANUAL_OVERRIDE, event,
                    self._build_override_handler(slave, event))

    def _build_override_handler(self, slave, event):
        log.debug('Building event handler for protocol: %s event: %s', slave,
                  event)

        def inner():
            return None, self._slave_protocols[slave]._protocol_fsm.on_event(
                event)

        return inner

    def register_slave_protocol(self, name, protocol):
        """
        @param name: slave protocol name
        @param protocol: slave protocol instance
        @return: None
        """
        self._slave_protocols[name] = protocol

    def _slave_protocol_event(self, event, *args, **kwargs):
        """
        Handle an event from a slave protocol.
        @param event: event to be processed
        """
        name = kwargs.get('name')
        if name is not None and name in self._slave_protocols:
            # only react to slave protocol events once we have transitioned out of unknown
            if self.get_current_state(
            ) != ProtocolState.UNKNOWN or event == DriverAsyncEvent.ERROR:
                if event == DriverAsyncEvent.STATE_CHANGE:
                    self._react()
                elif event == DriverAsyncEvent.CONFIG_CHANGE:
                    # do nothing, we handle this ourselves in set_param
                    pass
                else:
                    # pass the event up to the instrument agent
                    log.debug(
                        'Passing event up to the Instrument agent: %r %r %r',
                        event, args, kwargs)
                    self._driver_event(event, *args)

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        self._param_dict.add(
            Parameter.SAMPLE_INTERVAL,
            '',
            None,
            int,
            type=ParameterDictType.INT,
            display_name='Autosample Interval',
            description=
            'Interval between sample starts during autosample state',
            range=(7200, 86400),
            units=Units.SECOND)

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.ACQUIRE_SAMPLE,
                           display_name="Acquire Sample")
        self._cmd_dict.add(Capability.START_AUTOSAMPLE,
                           display_name="Start Autosample")
        self._cmd_dict.add(Capability.CALIBRATE,
                           display_name="Acquire Calibration Samples")
        self._cmd_dict.add(Capability.START_ION,
                           display_name="Start Ion Chamber Regeneration")
        self._cmd_dict.add(Capability.START_NAFION,
                           display_name="Start Nafion Regeneration")
        self._cmd_dict.add(Capability.STOP_REGEN,
                           display_name="Stop Current Regeneration")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE,
                           display_name="Stop Autosample")
        self._cmd_dict.add(Capability.POWEROFF, display_name='Low Power State')
        self._cmd_dict.add(Capability.GET_SLAVE_STATES,
                           display_name='Get Slave States')
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _react(self):
        """
        Determine if an action is necessary based on the states of the slave protocols.

            (MCU STATE, TURBO STATE, RGA STATE) : (TARGET, EVENT)

        The specified event will be sent to the specified target.
        """
        state = self.get_current_state()
        slave_states = self._get_slave_states()

        if MASSP_STATE_ERROR in slave_states:
            return self._error()

        if state == ProtocolState.REGEN and slave_states[
                0] == ProtocolState.COMMAND:
            self._async_raise_fsm_event(ProtocolEvent.REGEN_COMPLETE)

        # these actions are only applicable in POLL, AUTOSAMPLE or CALIBRATE states
        if state not in [
                ProtocolState.POLL, ProtocolState.AUTOSAMPLE,
                ProtocolState.CALIBRATE
        ]:
            return

        mps = mcu.ProtocolState
        tps = turbo.ProtocolState
        rps = rga.ProtocolState
        action_map = {
            # Waiting Turbo (RGA is off)
            (mps.WAITING_TURBO, tps.COMMAND, rps.COMMAND):
            (TURBO, turbo.Capability.START_TURBO),
            (mps.WAITING_TURBO, tps.AT_SPEED, rps.COMMAND):
            (MCU, mcu.Capability.START2),

            # Waiting RGA
            (mps.WAITING_RGA, tps.AT_SPEED, rps.SCAN): (MCU,
                                                        mcu.Capability.SAMPLE),
            (mps.WAITING_RGA, tps.AT_SPEED, rps.COMMAND):
            (RGA, rga.Capability.START_SCAN),
            (mps.WAITING_RGA, tps.COMMAND, rps.SCAN):
            (RGA, rga.Capability.STOP_SCAN),  # this should never happen!
            (mps.WAITING_RGA, tps.COMMAND, rps.COMMAND):
            (MCU, mcu.Capability.STANDBY),  # this should never happen!

            # Stopping
            (mps.STOPPING, tps.AT_SPEED, rps.SCAN): (RGA,
                                                     rga.Capability.STOP_SCAN),
            (mps.STOPPING, tps.AT_SPEED, rps.COMMAND):
            (TURBO, turbo.Capability.STOP_TURBO),
            (mps.STOPPING, tps.COMMAND, rps.SCAN):
            (RGA, rga.Capability.STOP_SCAN),  # this should never happen!
            (mps.STOPPING, tps.COMMAND, rps.COMMAND): (MCU,
                                                       mcu.Capability.STANDBY),
        }

        action = action_map.get(self._get_slave_states())

        if action is not None:
            if not isinstance(action, list):
                action = [action]

            # iterate through the action list, sending the events to the targets
            # if we are in POLL or CALIBRATE and we see a STANDBY event, return this driver to COMMAND.
            for target, command in action:
                if command == mcu.Capability.SAMPLE and state == ProtocolState.CALIBRATE:
                    command = mcu.Capability.CALIBRATE
                if command == mcu.Capability.STANDBY and state in [
                        ProtocolState.CALIBRATE, ProtocolState.POLL
                ]:
                    self._send_event_to_slave(target, command)
                    self._async_raise_fsm_event(ProtocolEvent.STOP)
                else:
                    self._send_event_to_slave(target, command)
        return action

    def _error(self):
        """
        Handle error state in slave protocol
        """
        state = self.get_current_state()
        slave_states = self._get_slave_states()

        # if we are not currently in the error state, make the transition
        if state != ProtocolState.ERROR:
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
        mcu_state, turbo_state, rga_state = slave_states

        # before we do anything else, the RGA must be stopped.
        if rga_state not in [
                rga.ProtocolState.COMMAND, rga.ProtocolState.ERROR
        ]:
            self._send_event_to_slave(RGA, rga.ProtocolEvent.STOP_SCAN)
        # RGA must be in COMMAND or ERROR, the TURBO must be stopped.
        elif turbo_state not in [
                turbo.ProtocolState.COMMAND, turbo.ProtocolState.SPINNING_DOWN
        ]:
            self._send_event_to_slave(TURBO, turbo.ProtocolEvent.STOP_TURBO)
        # Turbo and RGA must be in COMMAND or ERROR, stop the MCU
        elif mcu_state != mcu.ProtocolState.COMMAND:
            self._send_event_to_slave(MCU, mcu.ProtocolEvent.STANDBY)

    def _got_chunk(self, chunk):
        """
        This driver has no chunker...
        """

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events: Events to be filtered
        @return: list of events which are also capabilities
        """
        return [
            x for x in events if Capability.has(x) or mcu.Capability.has(x)
            or turbo.Capability.has(x) or rga.Capability.has(x)
        ]

    def _get_slave_states(self):
        """
        Retrieve the current protocol state from each of the slave protocols and return them as a tuple.
        """
        return (
            self._slave_protocols[MCU].get_current_state(),
            self._slave_protocols[TURBO].get_current_state(),
            self._slave_protocols[RGA].get_current_state(),
        )

    def _send_event_to_all(self, event):
        """
        Send the same event to all slave protocols.
        @return: List of (name, result) for a slave protocols
        """
        return [(name, slave._protocol_fsm.on_event(event))
                for name, slave in self._slave_protocols.items()]

    def _send_event_to_slave(self, name, event):
        """
        Send an event to a specific protocol
        @param name: Name of slave protocol
        @param event: Event to be sent
        """
        slave_protocol = self._slave_protocols.get(name)
        if slave_protocol is None:
            raise InstrumentProtocolException(
                'Attempted to send event to non-existent protocol: %s' % name)
        slave_protocol._async_raise_fsm_event(event)

    def _send_massp_direct_access(self, command):
        """
        Handle a direct access command.  Driver expects direct access commands to specify the target
        using the following format:

        target:command

        It then routes the command to the appropriate slave protocol.
        @param command: Direct access command received
        """
        err_string = 'Invalid command.  Command must be in the following format: "target:command' + NEWLINE + \
                     'Valid targets are: %r' % self._slave_protocols.keys()
        try:
            target, command = command.split(DA_COMMAND_DELIMITER, 1)
            target = target.lower()
        except ValueError:
            target = None

        log.debug('_do_cmd_direct - target: %s command: %r', target, command)

        if target not in self._slave_protocols:
            self._driver_event(DriverAsyncEvent.DIRECT_ACCESS, err_string)
        else:
            self._slave_protocols[target]._protocol_fsm.on_event(
                ProtocolEvent.EXECUTE_DIRECT, command)

    def _set_params(self, *args, **kwargs):
        """
        Set one or more parameters.  This method will determine where the parameter actually resides and
        forward it to the appropriate parameter dictionary based on name.
        @param args: arglist which must contain a parameter dictionary
        @throws InstrumentParameterException
        """
        params = args[0]

        if not isinstance(params, dict):
            raise InstrumentParameterException(
                'Attempted to set parameters with a non-dictionary argument')

        _, old_config = self._handler_command_get([Parameter.ALL])

        temp_dict = {}
        for key in params:
            split_key = key.split('_', 1)
            if len(split_key) == 1:
                raise InstrumentParameterException(
                    'Missing target in MASSP parameter: %s' % key)
            target = split_key[0]
            if target not in self._slave_protocols:
                # this is a master driver parameter, set it here
                if key in self._param_dict.get_keys():
                    log.debug("Setting value for %s to %s", key, params[key])
                    self._param_dict.set_value(key, params[key])
                else:
                    raise InstrumentParameterException(
                        'Invalid key in SET action: %s' % key)
            else:
                temp_dict.setdefault(target, {})[key] = params[key]

        # set parameters for slave protocols
        for name in temp_dict:
            if name in self._slave_protocols:
                self._slave_protocols[name]._set_params(temp_dict[name])
            else:
                # how did we get here?  This should never happen, but raise an exception if it does.
                raise InstrumentParameterException(
                    'Invalid key(s) in SET action: %r' % temp_dict[name])

        _, new_config = self._handler_command_get([Parameter.ALL])

        if not new_config == old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def set_init_params(self, config):
        """
        Set initial parameters.  Parameters are forwarded to the appropriate parameter dictionary based on name.
        @param config: Init param config to be handled
        """
        temp_dict = {}
        self._startup_config = config
        config = config.get(DriverConfigKey.PARAMETERS, {})
        for key in config:
            target, _ = key.split('_', 1)
            if target not in self._slave_protocols:
                # master driver parameter
                log.debug("Setting init value for %s to %s", key, config[key])
                self._param_dict.set_init_value(key, config[key])
            else:
                temp_dict.setdefault(target, {})[key] = config[key]

        for name in temp_dict:
            if name in self._slave_protocols:
                self._slave_protocols[name].set_init_params(
                    {DriverConfigKey.PARAMETERS: temp_dict[name]})
            else:
                # how did we get here?  This should never happen, but raise an exception if it does.
                raise InstrumentParameterException(
                    'Invalid key(s) in INIT PARAMS action: %r' %
                    temp_dict[name])

    def get_config_metadata_dict(self):
        """
        See base class for full description.  This method is overridden to retrieve the parameter
        dictionary from each slave protocol and merge them.
        @return: dictionary containing driver metadata
        """
        log.debug("Getting metadata dict from protocol...")
        return_dict = {
            ConfigMetadataKey.DRIVER: self._driver_dict.generate_dict(),
            ConfigMetadataKey.COMMANDS: self._cmd_dict.generate_dict(),
            ConfigMetadataKey.PARAMETERS: self._param_dict.generate_dict()
        }

        for protocol in self._slave_protocols.values():
            return_dict[ConfigMetadataKey.PARAMETERS].update(
                protocol._param_dict.generate_dict())
            return_dict[ConfigMetadataKey.COMMANDS].update(
                protocol._cmd_dict.generate_dict())

        return return_dict

    def get_resource_capabilities(self, current_state=True):
        """
        Overrides base class to include slave protocol parameters
        @param current_state: Boolean indicating whether we should return only the current state events
        @return: (resource_commands, resource_parameters)
        """
        res_cmds = self._protocol_fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)
        res_params = self._param_dict.get_keys()

        for protocol in self._slave_protocols.values():
            res_params.extend(protocol._param_dict.get_keys())

        return res_cmds, res_params

    def _build_scheduler(self):
        """
        Build a scheduler for periodic status updates
        """
        job_name = ScheduledJob.ACQUIRE_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.ACQUIRE_SAMPLE,
                                  ProtocolEvent.ACQUIRE_SAMPLE)

    def _delete_scheduler(self):
        """
        Remove the autosample schedule.
        """
        try:
            self._remove_scheduler(ScheduledJob.ACQUIRE_SAMPLE)
        except KeyError:
            log.info('Failed to remove scheduled job for ACQUIRE_SAMPLE')

    ########################################################################
    # Generic handlers.
    ########################################################################
    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler, raise STATE CHANGE
        """
        if self.get_current_state() != ProtocolState.UNKNOWN:
            self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler, do nothing.
        """

    def _handler_stop_generic(self, *args, **kwargs):
        """
        Generic stop method to return to COMMAND (via POLL if appropriate)
        @return next_state, (next_state, None)
        """
        next_state = ProtocolState.COMMAND
        result = []

        self._delete_scheduler()

        # check if we are in autosample AND currently taking a sample, if so, move to POLL
        # otherwise go back to COMMAND.
        if self.get_current_state() == ProtocolState.AUTOSAMPLE:
            if self._get_slave_states() != (ProtocolState.COMMAND,
                                            ProtocolState.COMMAND,
                                            ProtocolState.COMMAND):
                next_state = ProtocolState.POLL

        return next_state, (next_state, result)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = self._send_event_to_all(ProtocolEvent.DISCOVER)
        log.debug('_handler_unknown_discover -- send DISCOVER to all: %r',
                  result)
        target_state = (ProtocolState.COMMAND, ProtocolState.COMMAND,
                        ProtocolState.COMMAND)
        success = False
        # wait for the slave protocols to discover
        for attempt in xrange(5):
            slave_states = self._get_slave_states()
            if slave_states == target_state:
                success = True
                break
            time.sleep(1)
        if not success:
            next_state = ProtocolState.ERROR
        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameter.  Query this protocol plus all slave protocols.
        @param args: arglist which should contain a list of parameters to get
        @return None, results
        """
        params = args[0]

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

        temp_dict = {}
        result_dict = {}

        # request is for all parameters, send get(ALL) to each protocol then combine the results.
        if Parameter.ALL in params:
            params = [Parameter.ALL]
            _, result = self._handler_get(params, **kwargs)
            result_dict.update(result)
            for protocol in self._slave_protocols.values():
                _, result = protocol._handler_get(params, **kwargs)
                result_dict.update(result)

        # request is for specific parameters.  Determine which protocol should service each,
        # call the appropriate _handler_get and combine the results
        else:
            for key in params:
                log.debug('about to split: %s', key)
                target, _ = key.split('_', 1)
                temp_dict.setdefault(target, []).append(key)
            for key in temp_dict:
                if key == MASTER:
                    _, result = self._handler_get(params, **kwargs)
                else:
                    if key in self._slave_protocols:
                        _, result = self._slave_protocols[key]._handler_get(
                            params, **kwargs)
                    else:
                        raise InstrumentParameterException(
                            'Invalid key(s) in GET action: %r' %
                            temp_dict[key])
                result_dict.update(result)

        return None, result_dict

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter, just pass through to _set_params, which knows how to set the params
        in the slave protocols.
        """
        next_state = None
        result = []
        self._set_params(*args, **kwargs)
        return next_state, (next_state, result)

    def _handler_command_start_direct(self):
        """
        Start direct access
        """
        next_state = ProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    def _handler_command_start_autosample(self):
        """
        Move my FSM to autosample and start the sample sequence by sending START1 to the MCU.
        Create the scheduler to automatically start the next sample sequence
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        self._build_scheduler()
        return next_state, (next_state, result)

    def _handler_command_start_poll(self):
        """
        Move my FSM to poll and start the sample sequence by sending START1 to the MCU
        """
        next_state = ProtocolState.POLL
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        return next_state, (next_state, result)

    def _handler_command_start_calibrate(self):
        """
        Move my FSM to calibrate and start the calibrate sequence by sending START1 to the MCU
        """
        next_state = ProtocolState.CALIBRATE
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        return next_state, (next_state, result)

    def _handler_command_start_nafion_regen(self):
        """
        Move my FSM to NAFION_REGEN and send NAFION_REGEN to the MCU
        """
        next_state = ProtocolState.REGEN
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.NAFREG)
        return next_state, (next_state, result)

    def _handler_command_start_ion_regen(self):
        """
        Move my FSM to ION_REGEN and send ION_REGEN to the MCU
        """
        next_state = ProtocolState.REGEN
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.IONREG)
        return next_state, (next_state, result)

    def _handler_command_poweroff(self):
        """
        Send POWEROFF to the MCU
        """
        next_state = None
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.POWEROFF)
        return next_state, (next_state, result)

    def _handler_command_start_manual(self):
        """
        Move FSM to MANUAL OVERRIDE state
        """
        next_state = ProtocolState.MANUAL_OVERRIDE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Error handlers.
    ########################################################################

    def _handler_error(self):
        next_state = ProtocolState.ERROR
        result = []
        return next_state, (next_state, result)

    def _handler_error_clear(self):
        """
        Send the CLEAR event to any slave protocol in the error state and return this driver to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        for protocol in self._slave_protocols:
            state = protocol.get_current_state()
            if state == MASSP_STATE_ERROR:
                # do this synchronously, to allow each slave protocol to complete the CLEAR action
                # before transitioning states.
                protocol._protocol_fsm.on_event(ProtocolEvent.CLEAR)
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_acquire_sample(self):
        """
        Fire off a sample sequence while in the autosample state.
        @throws InstrumentProtocolException
        """
        next_state = None
        result = []
        slave_states = self._get_slave_states()
        # verify the MCU is not already in a sequence
        if slave_states[0] == ProtocolState.COMMAND:
            result = self._send_event_to_slave(MCU, mcu.Capability.START1)
        else:
            raise InstrumentProtocolException(
                "Attempted to acquire sample while sampling")
        return next_state, (next_state, result)

    ########################################################################
    # Direct access handlers.
    ########################################################################

    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access state.  Forward to all slave protocols.
        """
        self._send_event_to_all(ProtocolEvent.START_DIRECT)

        # 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.  Check slave protocol states and verify they all
        return to COMMAND, otherwise raise InstrumentProtocolException.
        @throws InstrumentProtocolException
        """
        for attempt in range(DA_EXIT_MAX_RETRIES):
            slave_states = self._get_slave_states()
            if ProtocolState.DIRECT_ACCESS in slave_states:
                log.error(
                    'Slave protocol failed to return to command, attempt %d',
                    attempt)
                time.sleep(1)
            else:
                return

        raise InstrumentProtocolException(
            'Slave protocol never returned to command from DA.')

    def _handler_direct_access_execute_direct(self, data):
        """
        Execute a direct access command.  For MASSP, this means passing the actual command to the
        correct slave protocol.  This is handled by _send_massp_direct_access.
        """
        self._send_massp_direct_access(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return None, (None, [])

    def _handler_direct_access_stop_direct(self):
        next_state = ProtocolState.COMMAND
        result = []
        self._send_event_to_all(ProtocolEvent.STOP_DIRECT)
        return next_state, (next_state, result)

    ########################################################################
    # Regen handlers.
    ########################################################################

    def _handler_stop_regen(self):
        """
        Abort the current regeneration sequence, return to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.STANDBY)
        return next_state, (next_state, result)

    def _handler_regen_complete(self):
        """
        Regeneration sequence is complete, return to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._async_agent_state_change(ResourceAgentState.COMMAND)
        return next_state, (next_state, result)

    def _handler_manual_override_stop(self):
        """
        Exit manual override.  Attempt to bring the slave drivers back to COMMAND.
        """
        next_state = ProtocolState.COMMAND
        result = []
        mcu_state, turbo_state, rga_state = self._get_slave_states()
        if rga_state == rga.ProtocolState.SCAN:
            self._slave_protocols[RGA]._protocol_fsm.on_event(
                rga.Capability.STOP_SCAN)
        if turbo_state == turbo.ProtocolState.AT_SPEED:
            self._slave_protocols[TURBO]._protocol_fsm.on_event(
                turbo.Capability.STOP_TURBO)
        while rga_state not in [rga.ProtocolState.COMMAND, rga.ProtocolState.ERROR] or \
                turbo_state not in [turbo.ProtocolState.COMMAND, turbo.ProtocolState.ERROR]:
            time.sleep(.1)
            mcu_state, turbo_state, rga_state = self._get_slave_states()
        if mcu_state != mcu.ProtocolState.COMMAND:
            self._slave_protocols[MCU]._protocol_fsm.on_event(
                mcu.Capability.STANDBY)

        return next_state, (next_state, result)

    def _handler_manual_get_slave_states(self):
        """
        Get the slave states and return them to the user
        @return: next_state, (next_state, result)
        """
        mcu_state, turbo_state, rga_state = self._get_slave_states()
        return None, (None, {
            MCU: mcu_state,
            RGA: rga_state,
            TURBO: turbo_state
        })
Beispiel #32
0
class SBE43Protocol(SBE16Protocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    def __init__(self, prompts, newline, driver_event):
        """
        SBE43Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The SBE43 newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build SBE19 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_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_command_acquire_sample),
                (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.CLOCK_SYNC, self._handler_command_clock_sync_clock),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_command_acquire_status)
            ],
            ProtocolState.ACQUIRING_SAMPLE: [
                (ProtocolEvent.ENTER, self._handler_acquiring_sample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_SAMPLE_ASYNC, self._handler_acquire_sample_async),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
                (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct)
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample),
                (ProtocolEvent.SCHEDULED_ACQUIRED_STATUS, self._handler_autosample_acquire_status),
            ]
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add build handlers for device commands, only using simple command handler.
        for cmd in Command.list():
            if cmd == Command.SET:
                self._add_build_handler(Command.SET, self._build_set_command)
            else:
                self._add_build_handler(cmd, self._build_simple_command)

        # Add response handlers for device commands.
        # these are here to ensure that correct responses to the commands are received before the next command is sent
        self._add_response_handler(Command.SET, self._parse_set_response)
        self._add_response_handler(Command.GET_SD, self._validate_GetSD_response)
        self._add_response_handler(Command.GET_HD, self._validate_GetHD_response)
        self._add_response_handler(Command.GET_CD, self._validate_GetCD_response)
        self._add_response_handler(Command.GET_CC, self._validate_GetCC_response)
        self._add_response_handler(Command.GET_EC, self._validate_GetEC_response)

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(self.sieve_function)

    def _build_command_dict(self):
        """
        Populate the command dictionary with command. Overridden to specify timeouts.
        """
        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, timeout=ACQUIRE_STATUS_TIMEOUT, display_name="Acquire Status")
        self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="Acquire Sample")
        self._cmd_dict.add(Capability.DISCOVER, timeout=DISCOVER_TIMEOUT, display_name='Discover')

    def _filter_capabilities(self, events):
        return [x for x in events if Capability.has(x)]

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        Over-ride sieve function to handle additional particles.
        """
        matchers = []
        return_list = []

        matchers.append(SBE43DataParticle.regex_compiled())
        matchers.append(SBE43HardwareParticle.regex_compiled())
        matchers.append(SBE43CalibrationParticle.regex_compiled())
        matchers.append(SBE43StatusParticle.regex_compiled())
        matchers.append(SBE43ConfigurationParticle.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):
        """
        Over-ride sieve function to handle additional particles.
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        if self._extract_sample(SBE43DataParticle, SBE43DataParticle.regex_compiled(), chunk, timestamp):
            self._sampling = True
            return

        for particle_class in SBE43HardwareParticle, \
                              SBE43CalibrationParticle, \
                              SBE43ConfigurationParticle, \
                              SBE43StatusParticle:
            if self._extract_sample(particle_class, particle_class.regex_compiled(), chunk, timestamp):
                return

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        self._verify_not_readonly(*args, **kwargs)
        update_params = False

        # check values that the instrument doesn't validate
        # handle special cases for driver specific parameters
        for (key, val) in params.iteritems():
            if key == Parameter.PUMP_DELAY and (val < MIN_PUMP_DELAY or val > MAX_PUMP_DELAY):
                raise InstrumentParameterException("pump delay out of range")
            elif key == Parameter.NUM_AVG_SAMPLES and (val < MIN_AVG_SAMPLES or val > MAX_AVG_SAMPLES):
                raise InstrumentParameterException("num average samples out of range")

        for (key, val) in params.iteritems():

            old_val = self._param_dict.format(key)
            new_val = self._param_dict.format(key, val)
            log.debug("KEY = %r OLD VALUE = %r NEW VALUE = %r", key, old_val, new_val)

            if old_val != new_val:
                update_params = True
                if ConfirmedParameter.has(key):
                    # We add a write delay here because this command has to be sent
                    # twice, the write delay allows it to process the first command
                    # before it receives the beginning of the second.
                    self._do_cmd_resp(Command.SET, key, val, write_delay=0.2)
                else:
                    self._do_cmd_resp(Command.SET, key, val, **kwargs)

        log.debug("set complete, update params")
        if update_params:
            self._update_params()

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_acquire_status(self, *args, **kwargs):
        """
        Get device status
        """
        next_state = None

        response = self._do_cmd_resp(Command.GET_SD, response_regex=SBE43StatusParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetSD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_HD, response_regex=SBE43HardwareParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetHD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_CD, response_regex=SBE43ConfigurationParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetCD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_CC, response_regex=SBE43CalibrationParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetCC Response: %s", response)
        response = self._do_cmd_resp(Command.GET_EC, timeout=TIMEOUT)
        log.debug("_handler_command_acquire_status: GetEC Response: %s", response)

        # Reset the event counter right after getEC
        self._do_cmd_resp(Command.RESET_EC, timeout=TIMEOUT)

        result = self.wait_for_particles([DataParticleType.DEVICE_STATUS, DataParticleType.DEVICE_CALIBRATION,
                                          DataParticleType.DEVICE_HARDWARE, DataParticleType.DEVICE_CONFIGURATION])

        return next_state, (next_state, result)

    def _handler_command_acquire_sample(self, *args, **kwargs):
        """
        Acquire Sample is implemented asynchronously. Transition to ACQUIRING_SAMPLE state.
        """
        next_state = ProtocolState.ACQUIRING_SAMPLE
        result = []

        return next_state, (next_state, result)

    def _handler_acquiring_sample_enter(self):
        """
        Trigger the ACQUIRE_SAMPLE_ASYNC event
        """
        self._async_raise_fsm_event(ProtocolEvent.ACQUIRE_SAMPLE_ASYNC)

    def _handler_acquire_sample_async(self, *args, **kwargs):
        """
        Acquire sample from SBE16.
        @retval next_state, (next_state, result) tuple
        """
        next_state = ProtocolState.COMMAND
        result = []

        self._do_cmd_resp(Command.TS, *args, timeout=ASYNC_TIMEOUT, **kwargs)

        return next_state, (next_state, result)

    def _handler_autosample_acquire_status(self, *args, **kwargs):
        """
        Get device status in autosample mode
        """
        next_state = None
        result = []

        # When in autosample this command requires two wake-ups to get to the right prompt
        self._wakeup(timeout=WAKEUP_TIMEOUT, delay=0.3)
        self._wakeup(timeout=WAKEUP_TIMEOUT, delay=0.3)

        response = self._do_cmd_resp(Command.GET_SD, response_regex=SBE43StatusParticle.regex_compiled(),
                                        timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetSD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_HD, response_regex=SBE43HardwareParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetHD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_CD, response_regex=SBE43ConfigurationParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetCD Response: %s", response)
        response = self._do_cmd_resp(Command.GET_CC, response_regex=SBE43CalibrationParticle.regex_compiled(),
                                     timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetCC Response: %s", response)
        response = self._do_cmd_resp(Command.GET_EC, timeout=TIMEOUT)
        log.debug("_handler_autosample_acquire_status: GetEC Response: %s", response)

        # Reset the event counter right after getEC
        self._do_cmd_no_resp(Command.RESET_EC)

        return next_state, (next_state, result)

    ########################################################################
    # response handlers.
    ########################################################################
    def _validate_GetSD_response(self, response, prompt):
        """
        validation handler for GetSD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("_validate_GetSD_response: GetSD command encountered error; type='%s' msg='%s'", error[0],
                      error[1])
            raise InstrumentProtocolException('GetSD command failure: type="%s" msg="%s"' % (error[0], error[1]))

        if not SBE43StatusParticle.resp_regex_compiled().search(response):
            log.error('_validate_GetSD_response: GetSD command not recognized: %s.' % response)
            raise InstrumentProtocolException('GetSD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetHD_response(self, response, prompt):
        """
        validation handler for GetHD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetHD command encountered error; type='%s' msg='%s'", error[0], error[1])
            raise InstrumentProtocolException('GetHD command failure: type="%s" msg="%s"' % (error[0], error[1]))

        if not SBE43HardwareParticle.resp_regex_compiled().search(response):
            log.error('_validate_GetHD_response: GetHD command not recognized: %s.' % response)
            raise InstrumentProtocolException('GetHD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetCD_response(self, response, prompt):
        """
        validation handler for GetCD command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetCD command encountered error; type='%s' msg='%s'", error[0], error[1])
            raise InstrumentProtocolException('GetCD command failure: type="%s" msg="%s"' % (error[0], error[1]))

        if not SBE43ConfigurationParticle.resp_regex_compiled().search(response):
            log.error('_validate_GetCD_response: GetCD command not recognized: %s.' % response)
            raise InstrumentProtocolException('GetCD command not recognized: %s.' % response)

        self._param_dict.update_many(response)

        return response

    def _validate_GetCC_response(self, response, prompt):
        """
        validation handler for GetCC command
        @param response command response string.
        @param prompt prompt following command response.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetCC command encountered error; type='%s' msg='%s'", error[0], error[1])
            raise InstrumentProtocolException('GetCC command failure: type="%s" msg="%s"' % (error[0], error[1]))

        if not SBE43CalibrationParticle.resp_regex_compiled().search(response):
            log.error('_validate_GetCC_response: GetCC command not recognized: %s.' % response)
            raise InstrumentProtocolException('GetCC command not recognized: %s.' % response)

        return response

    def _validate_GetEC_response(self, response, prompt):
        """
        validation handler for GetEC command
        @param response command response string.
        @throws InstrumentProtocolException if command misunderstood.
        """
        error = self._find_error(response)

        if error:
            log.error("GetEC command encountered error; type='%s' msg='%s'", error[0], error[1])
            raise InstrumentProtocolException('GetEC command failure: type="%s" msg="%s"' % (error[0], error[1]))

        return response

    ########################################################################
    # Private helpers.
    ########################################################################
    def _build_param_dict(self):
        """
        Populate the parameter dictionary with SBE19 parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        self._build_common_param_dict()

        self._param_dict.add(Parameter.SBE63,
                             r'SBE63>(.*)</SBE63',
                             lambda match: True if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="SBE63 Attached",
                             range={'True': True, 'False': False},
                             description="Enable SBE63: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.NUM_AVG_SAMPLES,
                             r'ScansToAverage>([\d]+)</ScansToAverage>',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.INT,
                             display_name="Scans to Average",
                             range=INT16,
                             description="Number of samples to average",
                             startup_param=True,
                             direct_access=False,
                             default_value=4,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self._param_dict.add(Parameter.MIN_COND_FREQ,
                             r'MinimumCondFreq>([\d]+)</MinimumCondFreq',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.INT,
                             display_name="Minimum Conductivity Frequency",
                             range=INT16,
                             description="Minimum conductivity frequency to enable pump turn-on.",
                             startup_param=True,
                             direct_access=False,
                             default_value=500,
                             units=Units.HERTZ,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.PUMP_DELAY,
                             r'PumpDelay>([\d]+)</PumpDelay',
                             lambda match: int(match.group(1)),
                             str,
                             type=ParameterDictType.INT,
                             display_name="Pump Delay",
                             range=INT16,
                             description="Time to wait after minimum conductivity frequency is reached before turning pump on.",
                             startup_param=True,
                             direct_access=False,
                             default_value=60,
                             units=Units.SECOND,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self._param_dict.add(Parameter.AUTO_RUN,
                             r'AutoRun>(.*)</AutoRun',
                             lambda match: True if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="Auto Run",
                             range={'True': True, 'False': False},
                             description="Enable automatic logging when power is applied: (true | false).",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.IGNORE_SWITCH,
                             r'IgnoreSwitch>(.*)</IgnoreSwitch',
                             lambda match: True if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="Ignore Switch",
                             range={'True': True, 'False': False},
                             description="Disable magnetic switch position for starting or stopping logging: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=True,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.PTYPE,
                             r"<Sensor id = 'Main Pressure'>.*?<type>(.*?)</type>.*?</Sensor>",
                             self._pressure_sensor_to_int,
                             str,
                             type=ParameterDictType.INT,
                             display_name="Pressure Sensor Type",
                             range={'Strain Gauge': 1, 'Quartz with Temp Comp': 3},
                             startup_param=True,
                             direct_access=True,
                             default_value=1,
                             description="Sensor type: (1:strain gauge | 3:quartz with temp comp)",
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             regex_flags=re.DOTALL)
        self._param_dict.add(Parameter.OPTODE,
                             r'OPTODE>(.*)</OPTODE',
                             lambda match: True if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="Optode Attached",
                             range={'True': True, 'False': False},
                             description="Enable optode: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
        self._param_dict.add(Parameter.VOLT1,
                             r'ExtVolt1>(.*)</ExtVolt1',
                             lambda match: True if match.group(1) == 'yes' else False,
                             self._true_false_to_string,
                             type=ParameterDictType.BOOL,
                             display_name="Volt 1",
                             range={'True': True, 'False': False},
                             description="Enable external voltage 1: (true | false)",
                             startup_param=True,
                             direct_access=True,
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE)
Beispiel #33
0
class Protocol(InstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, driver_event):
        """
        Protocol constructor.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        InstrumentProtocol.__init__(self, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample),
                (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_command_start_poll),
                (ProtocolEvent.CALIBRATE, self._handler_command_start_calibrate),
                (ProtocolEvent.START_NAFION, self._handler_command_start_nafion_regen),
                (ProtocolEvent.START_ION, self._handler_command_start_ion_regen),
                (ProtocolEvent.ERROR, self._handler_error),
                (ProtocolEvent.POWEROFF, self._handler_command_poweroff),
                (ProtocolEvent.START_MANUAL, self._handler_command_start_manual),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_SAMPLE, self._handler_autosample_acquire_sample),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.POLL: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.CLEAR, self._handler_error_clear),
            ],
            ProtocolState.CALIBRATE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP, self._handler_stop_generic),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.REGEN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_REGEN, self._handler_stop_regen),
                (ProtocolEvent.REGEN_COMPLETE, self._handler_regen_complete),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_direct_access_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
            ],
            ProtocolState.MANUAL_OVERRIDE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_MANUAL, self._handler_manual_override_stop),
                (ProtocolEvent.GET_SLAVE_STATES, self._handler_manual_get_slave_states),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # 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()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._slave_protocols = {}
        self.initialize_scheduler()

    def _add_manual_override_handlers(self):
        for slave in self._slave_protocols:
            for event in self._slave_protocols[slave]._cmd_dict._cmd_dict:
                self._protocol_fsm.add_handler(ProtocolState.MANUAL_OVERRIDE,
                                               event, self._build_override_handler(slave, event))

    def _build_override_handler(self, slave, event):
        log.debug('Building event handler for protocol: %s event: %s', slave, event)

        def inner():
            return None, self._slave_protocols[slave]._protocol_fsm.on_event(event)
        return inner

    def register_slave_protocol(self, name, protocol):
        """
        @param name: slave protocol name
        @param protocol: slave protocol instance
        @return: None
        """
        self._slave_protocols[name] = protocol

    def _slave_protocol_event(self, event, *args, **kwargs):
        """
        Handle an event from a slave protocol.
        @param event: event to be processed
        """
        name = kwargs.get('name')
        if name is not None and name in self._slave_protocols:
            # only react to slave protocol events once we have transitioned out of unknown
            if self.get_current_state() != ProtocolState.UNKNOWN or event == DriverAsyncEvent.ERROR:
                if event == DriverAsyncEvent.STATE_CHANGE:
                    self._react()
                elif event == DriverAsyncEvent.CONFIG_CHANGE:
                    # do nothing, we handle this ourselves in set_param
                    pass
                else:
                    # pass the event up to the instrument agent
                    log.debug('Passing event up to the Instrument agent: %r %r %r', event, args, kwargs)
                    self._driver_event(event, *args)

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        self._param_dict.add(Parameter.SAMPLE_INTERVAL, '', None, int,
                             type=ParameterDictType.INT,
                             display_name='Autosample Interval',
                             description='Interval between sample starts during autosample state',
                             range=(7200, 86400),
                             units=Units.SECOND)

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.ACQUIRE_SAMPLE, display_name="Acquire Sample")
        self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample")
        self._cmd_dict.add(Capability.CALIBRATE, display_name="Acquire Calibration Samples")
        self._cmd_dict.add(Capability.START_ION, display_name="Start Ion Chamber Regeneration")
        self._cmd_dict.add(Capability.START_NAFION, display_name="Start Nafion Regeneration")
        self._cmd_dict.add(Capability.STOP_REGEN, display_name="Stop Current Regeneration")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample")
        self._cmd_dict.add(Capability.POWEROFF, display_name='Low Power State')
        self._cmd_dict.add(Capability.GET_SLAVE_STATES,
                           display_name='Get Slave States')
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _react(self):
        """
        Determine if an action is necessary based on the states of the slave protocols.

            (MCU STATE, TURBO STATE, RGA STATE) : (TARGET, EVENT)

        The specified event will be sent to the specified target.
        """
        state = self.get_current_state()
        slave_states = self._get_slave_states()

        if MASSP_STATE_ERROR in slave_states:
            return self._error()

        if state == ProtocolState.REGEN and slave_states[0] == ProtocolState.COMMAND:
            self._async_raise_fsm_event(ProtocolEvent.REGEN_COMPLETE)

        # these actions are only applicable in POLL, AUTOSAMPLE or CALIBRATE states
        if state not in [ProtocolState.POLL, ProtocolState.AUTOSAMPLE, ProtocolState.CALIBRATE]:
            return

        mps = mcu.ProtocolState
        tps = turbo.ProtocolState
        rps = rga.ProtocolState
        action_map = {
            # Waiting Turbo (RGA is off)
            (mps.WAITING_TURBO, tps.COMMAND, rps.COMMAND): (TURBO, turbo.Capability.START_TURBO),
            (mps.WAITING_TURBO, tps.AT_SPEED, rps.COMMAND): (MCU, mcu.Capability.START2),

            # Waiting RGA
            (mps.WAITING_RGA, tps.AT_SPEED, rps.SCAN): (MCU, mcu.Capability.SAMPLE),
            (mps.WAITING_RGA, tps.AT_SPEED, rps.COMMAND): (RGA, rga.Capability.START_SCAN),
            (mps.WAITING_RGA, tps.COMMAND, rps.SCAN): (RGA, rga.Capability.STOP_SCAN),  # this should never happen!
            (mps.WAITING_RGA, tps.COMMAND, rps.COMMAND): (MCU, mcu.Capability.STANDBY),  # this should never happen!

            # Stopping
            (mps.STOPPING, tps.AT_SPEED, rps.SCAN): (RGA, rga.Capability.STOP_SCAN),
            (mps.STOPPING, tps.AT_SPEED, rps.COMMAND): (TURBO, turbo.Capability.STOP_TURBO),
            (mps.STOPPING, tps.COMMAND, rps.SCAN): (RGA, rga.Capability.STOP_SCAN),  # this should never happen!
            (mps.STOPPING, tps.COMMAND, rps.COMMAND): (MCU, mcu.Capability.STANDBY),
        }

        action = action_map.get(self._get_slave_states())

        if action is not None:
            if not isinstance(action, list):
                action = [action]

            # iterate through the action list, sending the events to the targets
            # if we are in POLL or CALIBRATE and we see a STANDBY event, return this driver to COMMAND.
            for target, command in action:
                if command == mcu.Capability.SAMPLE and state == ProtocolState.CALIBRATE:
                    command = mcu.Capability.CALIBRATE
                if command == mcu.Capability.STANDBY and state in [ProtocolState.CALIBRATE, ProtocolState.POLL]:
                    self._send_event_to_slave(target, command)
                    self._async_raise_fsm_event(ProtocolEvent.STOP)
                else:
                    self._send_event_to_slave(target, command)
        return action

    def _error(self):
        """
        Handle error state in slave protocol
        """
        state = self.get_current_state()
        slave_states = self._get_slave_states()

        # if we are not currently in the error state, make the transition
        if state != ProtocolState.ERROR:
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
        mcu_state, turbo_state, rga_state = slave_states

        # before we do anything else, the RGA must be stopped.
        if rga_state not in [rga.ProtocolState.COMMAND, rga.ProtocolState.ERROR]:
            self._send_event_to_slave(RGA, rga.ProtocolEvent.STOP_SCAN)
        # RGA must be in COMMAND or ERROR, the TURBO must be stopped.
        elif turbo_state not in [turbo.ProtocolState.COMMAND, turbo.ProtocolState.SPINNING_DOWN]:
            self._send_event_to_slave(TURBO, turbo.ProtocolEvent.STOP_TURBO)
        # Turbo and RGA must be in COMMAND or ERROR, stop the MCU
        elif mcu_state != mcu.ProtocolState.COMMAND:
            self._send_event_to_slave(MCU, mcu.ProtocolEvent.STANDBY)

    def _got_chunk(self, chunk):
        """
        This driver has no chunker...
        """

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events: Events to be filtered
        @return: list of events which are also capabilities
        """
        return [x for x in events if Capability.has(x) or mcu.Capability.has(x) or turbo.Capability.has(x) or
                rga.Capability.has(x)]

    def _get_slave_states(self):
        """
        Retrieve the current protocol state from each of the slave protocols and return them as a tuple.
        """
        return (
            self._slave_protocols[MCU].get_current_state(),
            self._slave_protocols[TURBO].get_current_state(),
            self._slave_protocols[RGA].get_current_state(),
        )

    def _send_event_to_all(self, event):
        """
        Send the same event to all slave protocols.
        @return: List of (name, result) for a slave protocols
        """
        return [(name, slave._protocol_fsm.on_event(event)) for name, slave in self._slave_protocols.items()]

    def _send_event_to_slave(self, name, event):
        """
        Send an event to a specific protocol
        @param name: Name of slave protocol
        @param event: Event to be sent
        """
        slave_protocol = self._slave_protocols.get(name)
        if slave_protocol is None:
            raise InstrumentProtocolException('Attempted to send event to non-existent protocol: %s' % name)
        slave_protocol._async_raise_fsm_event(event)

    def _send_massp_direct_access(self, command):
        """
        Handle a direct access command.  Driver expects direct access commands to specify the target
        using the following format:

        target:command

        It then routes the command to the appropriate slave protocol.
        @param command: Direct access command received
        """
        err_string = 'Invalid command.  Command must be in the following format: "target:command' + NEWLINE + \
                     'Valid targets are: %r' % self._slave_protocols.keys()
        try:
            target, command = command.split(DA_COMMAND_DELIMITER, 1)
            target = target.lower()
        except ValueError:
            target = None

        log.debug('_do_cmd_direct - target: %s command: %r', target, command)

        if target not in self._slave_protocols:
            self._driver_event(DriverAsyncEvent.DIRECT_ACCESS, err_string)
        else:
            self._slave_protocols[target]._protocol_fsm.on_event(ProtocolEvent.EXECUTE_DIRECT, command)

    def _set_params(self, *args, **kwargs):
        """
        Set one or more parameters.  This method will determine where the parameter actually resides and
        forward it to the appropriate parameter dictionary based on name.
        @param args: arglist which must contain a parameter dictionary
        @throws InstrumentParameterException
        """
        params = args[0]

        if not isinstance(params, dict):
            raise InstrumentParameterException('Attempted to set parameters with a non-dictionary argument')

        _, old_config = self._handler_command_get([Parameter.ALL])

        temp_dict = {}
        for key in params:
            split_key = key.split('_', 1)
            if len(split_key) == 1:
                raise InstrumentParameterException('Missing target in MASSP parameter: %s' % key)
            target = split_key[0]
            if target not in self._slave_protocols:
                # this is a master driver parameter, set it here
                if key in self._param_dict.get_keys():
                    log.debug("Setting value for %s to %s", key, params[key])
                    self._param_dict.set_value(key, params[key])
                else:
                    raise InstrumentParameterException('Invalid key in SET action: %s' % key)
            else:
                temp_dict.setdefault(target, {})[key] = params[key]

        # set parameters for slave protocols
        for name in temp_dict:
            if name in self._slave_protocols:
                self._slave_protocols[name]._set_params(temp_dict[name])
            else:
                # how did we get here?  This should never happen, but raise an exception if it does.
                raise InstrumentParameterException('Invalid key(s) in SET action: %r' % temp_dict[name])

        _, new_config = self._handler_command_get([Parameter.ALL])

        if not new_config == old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def set_init_params(self, config):
        """
        Set initial parameters.  Parameters are forwarded to the appropriate parameter dictionary based on name.
        @param config: Init param config to be handled
        """
        temp_dict = {}
        self._startup_config = config
        config = config.get(DriverConfigKey.PARAMETERS, {})
        for key in config:
            target, _ = key.split('_', 1)
            if target not in self._slave_protocols:
                # master driver parameter
                log.debug("Setting init value for %s to %s", key, config[key])
                self._param_dict.set_init_value(key, config[key])
            else:
                temp_dict.setdefault(target, {})[key] = config[key]

        for name in temp_dict:
            if name in self._slave_protocols:
                self._slave_protocols[name].set_init_params({DriverConfigKey.PARAMETERS: temp_dict[name]})
            else:
                # how did we get here?  This should never happen, but raise an exception if it does.
                raise InstrumentParameterException('Invalid key(s) in INIT PARAMS action: %r' % temp_dict[name])

    def get_config_metadata_dict(self):
        """
        See base class for full description.  This method is overridden to retrieve the parameter
        dictionary from each slave protocol and merge them.
        @return: dictionary containing driver metadata
        """
        log.debug("Getting metadata dict from protocol...")
        return_dict = {ConfigMetadataKey.DRIVER: self._driver_dict.generate_dict(),
                       ConfigMetadataKey.COMMANDS: self._cmd_dict.generate_dict(),
                       ConfigMetadataKey.PARAMETERS: self._param_dict.generate_dict()}

        for protocol in self._slave_protocols.values():
            return_dict[ConfigMetadataKey.PARAMETERS].update(protocol._param_dict.generate_dict())
            return_dict[ConfigMetadataKey.COMMANDS].update(protocol._cmd_dict.generate_dict())

        return return_dict

    def get_resource_capabilities(self, current_state=True):
        """
        Overrides base class to include slave protocol parameters
        @param current_state: Boolean indicating whether we should return only the current state events
        @return: (resource_commands, resource_parameters)
        """
        res_cmds = self._protocol_fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)
        res_params = self._param_dict.get_keys()

        for protocol in self._slave_protocols.values():
            res_params.extend(protocol._param_dict.get_keys())

        return res_cmds, res_params

    def _build_scheduler(self):
        """
        Build a scheduler for periodic status updates
        """
        job_name = ScheduledJob.ACQUIRE_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.ACQUIRE_SAMPLE, ProtocolEvent.ACQUIRE_SAMPLE)

    def _delete_scheduler(self):
        """
        Remove the autosample schedule.
        """
        try:
            self._remove_scheduler(ScheduledJob.ACQUIRE_SAMPLE)
        except KeyError:
            log.info('Failed to remove scheduled job for ACQUIRE_SAMPLE')

    ########################################################################
    # Generic handlers.
    ########################################################################
    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler, raise STATE CHANGE
        """
        if self.get_current_state() != ProtocolState.UNKNOWN:
            self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler, do nothing.
        """

    def _handler_stop_generic(self, *args, **kwargs):
        """
        Generic stop method to return to COMMAND (via POLL if appropriate)
        @return next_state, (next_state, None)
        """
        next_state = ProtocolState.COMMAND
        result = []

        self._delete_scheduler()

        # check if we are in autosample AND currently taking a sample, if so, move to POLL
        # otherwise go back to COMMAND.
        if self.get_current_state() == ProtocolState.AUTOSAMPLE:
            if self._get_slave_states() != (ProtocolState.COMMAND, ProtocolState.COMMAND, ProtocolState.COMMAND):
                next_state = ProtocolState.POLL

        return next_state, (next_state, result)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @return next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = self._send_event_to_all(ProtocolEvent.DISCOVER)
        log.debug('_handler_unknown_discover -- send DISCOVER to all: %r', result)
        target_state = (ProtocolState.COMMAND, ProtocolState.COMMAND, ProtocolState.COMMAND)
        success = False
        # wait for the slave protocols to discover
        for attempt in xrange(5):
            slave_states = self._get_slave_states()
            if slave_states == target_state:
                success = True
                break
            time.sleep(1)
        if not success:
            next_state = ProtocolState.ERROR
        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameter.  Query this protocol plus all slave protocols.
        @param args: arglist which should contain a list of parameters to get
        @return None, results
        """
        params = args[0]

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

        temp_dict = {}
        result_dict = {}

        # request is for all parameters, send get(ALL) to each protocol then combine the results.
        if Parameter.ALL in params:
            params = [Parameter.ALL]
            _, result = self._handler_get(params, **kwargs)
            result_dict.update(result)
            for protocol in self._slave_protocols.values():
                _, result = protocol._handler_get(params, **kwargs)
                result_dict.update(result)

        # request is for specific parameters.  Determine which protocol should service each,
        # call the appropriate _handler_get and combine the results
        else:
            for key in params:
                log.debug('about to split: %s', key)
                target, _ = key.split('_', 1)
                temp_dict.setdefault(target, []).append(key)
            for key in temp_dict:
                if key == MASTER:
                    _, result = self._handler_get(params, **kwargs)
                else:
                    if key in self._slave_protocols:
                        _, result = self._slave_protocols[key]._handler_get(params, **kwargs)
                    else:
                        raise InstrumentParameterException('Invalid key(s) in GET action: %r' % temp_dict[key])
                result_dict.update(result)

        return None, result_dict

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter, just pass through to _set_params, which knows how to set the params
        in the slave protocols.
        """
        next_state = None
        result = []
        self._set_params(*args, **kwargs)
        return next_state, (next_state, result)

    def _handler_command_start_direct(self):
        """
        Start direct access
        """
        next_state = ProtocolState.DIRECT_ACCESS
        result = []
        return next_state, (next_state, result)

    def _handler_command_start_autosample(self):
        """
        Move my FSM to autosample and start the sample sequence by sending START1 to the MCU.
        Create the scheduler to automatically start the next sample sequence
        """
        next_state = ProtocolState.AUTOSAMPLE
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        self._build_scheduler()
        return next_state, (next_state, result)

    def _handler_command_start_poll(self):
        """
        Move my FSM to poll and start the sample sequence by sending START1 to the MCU
        """
        next_state = ProtocolState.POLL
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        return next_state, (next_state, result)

    def _handler_command_start_calibrate(self):
        """
        Move my FSM to calibrate and start the calibrate sequence by sending START1 to the MCU
        """
        next_state = ProtocolState.CALIBRATE
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.START1)
        return next_state, (next_state, result)

    def _handler_command_start_nafion_regen(self):
        """
        Move my FSM to NAFION_REGEN and send NAFION_REGEN to the MCU
        """
        next_state = ProtocolState.REGEN
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.NAFREG)
        return next_state, (next_state, result)

    def _handler_command_start_ion_regen(self):
        """
        Move my FSM to ION_REGEN and send ION_REGEN to the MCU
        """
        next_state = ProtocolState.REGEN
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.IONREG)
        return next_state, (next_state, result)

    def _handler_command_poweroff(self):
        """
        Send POWEROFF to the MCU
        """
        next_state = None
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.POWEROFF)
        return next_state, (next_state, result)

    def _handler_command_start_manual(self):
        """
        Move FSM to MANUAL OVERRIDE state
        """
        next_state = ProtocolState.MANUAL_OVERRIDE
        result = []
        return next_state, (next_state, result)

    ########################################################################
    # Error handlers.
    ########################################################################

    def _handler_error(self):
        next_state = ProtocolState.ERROR
        result = []
        return next_state, (next_state, result)

    def _handler_error_clear(self):
        """
        Send the CLEAR event to any slave protocol in the error state and return this driver to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        for protocol in self._slave_protocols:
            state = protocol.get_current_state()
            if state == MASSP_STATE_ERROR:
                # do this synchronously, to allow each slave protocol to complete the CLEAR action
                # before transitioning states.
                protocol._protocol_fsm.on_event(ProtocolEvent.CLEAR)
        return next_state, (next_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_acquire_sample(self):
        """
        Fire off a sample sequence while in the autosample state.
        @throws InstrumentProtocolException
        """
        next_state = None
        result = []
        slave_states = self._get_slave_states()
        # verify the MCU is not already in a sequence
        if slave_states[0] == ProtocolState.COMMAND:
            result = self._send_event_to_slave(MCU, mcu.Capability.START1)
        else:
            raise InstrumentProtocolException("Attempted to acquire sample while sampling")
        return next_state, (next_state, result)

    ########################################################################
    # Direct access handlers.
    ########################################################################

    def _handler_direct_access_enter(self, *args, **kwargs):
        """
        Enter direct access state.  Forward to all slave protocols.
        """
        self._send_event_to_all(ProtocolEvent.START_DIRECT)

        # 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.  Check slave protocol states and verify they all
        return to COMMAND, otherwise raise InstrumentProtocolException.
        @throws InstrumentProtocolException
        """
        for attempt in range(DA_EXIT_MAX_RETRIES):
            slave_states = self._get_slave_states()
            if ProtocolState.DIRECT_ACCESS in slave_states:
                log.error('Slave protocol failed to return to command, attempt %d', attempt)
                time.sleep(1)
            else:
                return

        raise InstrumentProtocolException('Slave protocol never returned to command from DA.')

    def _handler_direct_access_execute_direct(self, data):
        """
        Execute a direct access command.  For MASSP, this means passing the actual command to the
        correct slave protocol.  This is handled by _send_massp_direct_access.
        """
        self._send_massp_direct_access(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return None, (None, [])

    def _handler_direct_access_stop_direct(self):
        next_state = ProtocolState.COMMAND
        result = []
        self._send_event_to_all(ProtocolEvent.STOP_DIRECT)
        return next_state, (next_state, result)

    ########################################################################
    # Regen handlers.
    ########################################################################

    def _handler_stop_regen(self):
        """
        Abort the current regeneration sequence, return to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._send_event_to_slave(MCU, mcu.Capability.STANDBY)
        return next_state, (next_state, result)

    def _handler_regen_complete(self):
        """
        Regeneration sequence is complete, return to COMMAND
        """
        next_state = ProtocolState.COMMAND
        result = []
        self._async_agent_state_change(ResourceAgentState.COMMAND)
        return next_state, (next_state, result)

    def _handler_manual_override_stop(self):
        """
        Exit manual override.  Attempt to bring the slave drivers back to COMMAND.
        """
        next_state = ProtocolState.COMMAND
        result = []
        mcu_state, turbo_state, rga_state = self._get_slave_states()
        if rga_state == rga.ProtocolState.SCAN:
            self._slave_protocols[RGA]._protocol_fsm.on_event(rga.Capability.STOP_SCAN)
        if turbo_state == turbo.ProtocolState.AT_SPEED:
            self._slave_protocols[TURBO]._protocol_fsm.on_event(turbo.Capability.STOP_TURBO)
        while rga_state not in [rga.ProtocolState.COMMAND, rga.ProtocolState.ERROR] or \
                turbo_state not in [turbo.ProtocolState.COMMAND, turbo.ProtocolState.ERROR]:
            time.sleep(.1)
            mcu_state, turbo_state, rga_state = self._get_slave_states()
        if mcu_state != mcu.ProtocolState.COMMAND:
            self._slave_protocols[MCU]._protocol_fsm.on_event(mcu.Capability.STANDBY)

        return next_state, (next_state, result)

    def _handler_manual_get_slave_states(self):
        """
        Get the slave states and return them to the user
        @return: next_state, (next_state, result)
        """
        mcu_state, turbo_state, rga_state = self._get_slave_states()
        return None, (None, {MCU: mcu_state, RGA: rga_state, TURBO: turbo_state})
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
                
            
Beispiel #35
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = METALOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.

        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT,
                 self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START1, self._handler_command_start1),
                (ProtocolEvent.NAFREG, self._handler_command_nafreg),
                (ProtocolEvent.IONREG, self._handler_command_ionreg),
                (ProtocolEvent.POWEROFF, self._handler_command_poweroff),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.START1: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START1_COMPLETE, self._handler_start1_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.WAITING_TURBO: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.START2, self._handler_waiting_turbo_start2),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.START2: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START2_COMPLETE, self._handler_start2_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.WAITING_RGA: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.SAMPLE, self._handler_waiting_rga_sample),
                (ProtocolEvent.CALIBRATE, self._handler_waiting_rga_cal),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.SAMPLE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.SAMPLE_COMPLETE, self._handler_sample_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.CALIBRATE: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.CALIBRATE_COMPLETE, self._handler_cal_complete),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.STOPPING: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.REGEN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_stop),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_stop),
                (ProtocolEvent.EXECUTE_DIRECT,
                 self._handler_direct_access_execute_direct),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STANDBY, self._handler_error_standby),
                (ProtocolEvent.CLEAR, self._handler_stop),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # response handlers
        for command in InstrumentCommand.list():
            self._add_response_handler(
                command,
                functools.partial(self._generic_response_handler,
                                  command=command))

        # 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()

        # Add build handlers for device commands.
        for command in InstrumentCommand.list():
            if command == InstrumentCommand.SET_TELEGRAM_INTERVAL:
                self._add_build_handler(command,
                                        self._build_telegram_interval_command)
            elif command == InstrumentCommand.SAMPLE:
                self._add_build_handler(command, self._build_sample_command)
            elif command == InstrumentCommand.SET_MINUTE:
                self._add_build_handler(command,
                                        self._build_set_minute_command)
            else:
                self._add_build_handler(command, self._build_simple_command)

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(Protocol.sieve_function)

        self.resetting = False

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        @param raw_data - data to be searched
        """
        matchers = []
        return_list = []

        matchers.append(McuDataParticle.regex_compiled())
        matchers.append(re.compile(r'(M .*?)(?=\r)'))
        matchers.append(re.compile(r'(E\d{3}.*?)(?=\r)'))

        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _build_param_dict(self):
        """
        Build the parameter dictionary
        """
        self._param_dict.add(
            Parameter.TELEGRAM_INTERVAL,
            '',
            None,
            None,
            type=ParameterDictType.INT,
            startup_param=True,
            display_name='Data Telegram Interval in Sample',
            units=Prefixes.MILLI + Units.SECOND,
            description='The interval between successive MCU data telegrams' +
            ' while in the SAMPLE/CAL state: (1 - 30000)')
        self._param_dict.add(
            Parameter.SAMPLE_TIME,
            '',
            None,
            None,
            type=ParameterDictType.INT,
            startup_param=True,
            display_name='Sample Cycle Time',
            units=Units.MINUTE,
            description=
            'The length of each portion of the sample cycle: (1 - 99)')
        self._param_dict.add(
            Parameter.ONE_MINUTE,
            '',
            None,
            None,
            visibility=ParameterDictVisibility.IMMUTABLE,
            type=ParameterDictType.INT,
            startup_param=True,
            default_value=60000,
            display_name='Length of One Minute',
            units=Prefixes.MILLI + Units.SECOND,
            description=
            'MCU timing constant representing the number of seconds per minute: (1 - 99999)'
        )
        self._param_dict.add(Parameter.ERROR_REASON,
                             '',
                             None,
                             None,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             type=ParameterDictType.STRING,
                             value='',
                             display_name='Reason for Error State',
                             description='MCU reason for error state.')

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START1, display_name="Execute ASTART1")
        self._cmd_dict.add(Capability.START2, display_name="Execute ASTART2")
        self._cmd_dict.add(Capability.SAMPLE, display_name="Execute ASAMPLEXX")
        self._cmd_dict.add(Capability.CALIBRATE, display_name="Execute ACAL9")
        self._cmd_dict.add(Capability.NAFREG,
                           display_name="Execute U ANAFREG3")
        self._cmd_dict.add(Capability.IONREG,
                           display_name="Execute U AIONREG3")
        self._cmd_dict.add(Capability.STANDBY,
                           display_name="Execute U ASTANDBY")
        self._cmd_dict.add(Capability.CLEAR, display_name="Clear Error State")
        self._cmd_dict.add(Capability.POWEROFF,
                           display_name="Execute U APOWEROFF")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_telegram_interval_command(self, *args, **kwargs):
        """
        Build the telegram interval command using the TELEGRAM_INTERVAL parameter
        """
        return '%s%08d%s' % (
            InstrumentCommand.SET_TELEGRAM_INTERVAL,
            int(self._param_dict.get(Parameter.TELEGRAM_INTERVAL)), NEWLINE)

    def _build_set_minute_command(self, *args, **kwargs):
        """
        Build the SETMINUTE command
        """
        return '%s%05d%s' % (InstrumentCommand.SET_MINUTE,
                             int(self._param_dict.get(
                                 Parameter.ONE_MINUTE)), NEWLINE)

    def _build_sample_command(self, *args, **kwargs):
        """
        Build the SAMPLE command
        """
        return '%s%02d%s' % (InstrumentCommand.SAMPLE,
                             int(self._param_dict.get(
                                 Parameter.SAMPLE_TIME)), NEWLINE)

    def _got_chunk(self, chunk, ts):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and regexes.

        Raise specific events on receipt of chunks.  This allows the driver to react asynchronously.

        @param chunk - data to be converted to a particle
        @param ts - timestamp
        """
        event = None
        exception = None
        sample = self._extract_sample(McuDataParticle,
                                      McuDataParticle.regex_compiled(), chunk,
                                      ts)
        if sample:
            return

        # we don't want to act on any responses in direct access or command mode
        # so just return here if that's the case...
        current_state = self.get_current_state()
        if current_state in [
                ProtocolState.DIRECT_ACCESS, ProtocolState.COMMAND
        ]:
            return

        # These responses (may) come from the instrument asynchronously, so they are handled
        # here rather than in a response handler.
        ignored = [Prompt.OK, Prompt.BEAT, Prompt.STANDBY]
        if chunk in ignored:
            pass
        elif chunk == Prompt.START1:
            event = ProtocolEvent.START1_COMPLETE
        elif chunk == Prompt.START2:
            event = ProtocolEvent.START2_COMPLETE
        elif chunk == Prompt.SAMPLE_FINISHED:
            event = ProtocolEvent.SAMPLE_COMPLETE
        elif chunk == Prompt.CAL_FINISHED:
            event = ProtocolEvent.CALIBRATE_COMPLETE
        elif chunk == Prompt.IONREG_FINISHED:
            event = ProtocolEvent.STANDBY
        elif chunk == Prompt.NAFREG_FINISHED:
            event = ProtocolEvent.STANDBY
        elif chunk == Prompt.ERROR:
            event = ProtocolEvent.ERROR
            self._param_dict.set_value(
                Parameter.ERROR_REASON,
                'Error prompt received from instrument.')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        elif chunk == Prompt.ONLINE:
            if not self.resetting:
                # This is an unexpected reset, ignore if we are in command or error
                if current_state == ProtocolState.ERROR:
                    event = ProtocolEvent.ERROR
                    self._param_dict.set_value(Parameter.ERROR_REASON,
                                               'MCU reset during sequence.')
                    self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        elif chunk in [
                Prompt.NAFTEMP_NOT_ACHIEVED, Prompt.IONTEMP_NOT_ACHIEVED
        ]:
            # regeneration temperature not achieved, move to COMMAND and raise an exception
            event = ProtocolEvent.STANDBY
            exception = InstrumentProtocolException(
                'Failed to achieve regen temperature')
        else:
            log.error('Unhandled chunk: %r in state: %s', chunk, current_state)
            exception = InstrumentProtocolException(
                'Unhandled chunk: %r in state: %s' % (chunk, current_state))

        if event is not None:
            self._async_raise_fsm_event(event)
        if exception:
            self._driver_event(DriverAsyncEvent.ERROR, exception)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events - events to be filtered
        @return list of events which are also capabilities
        """
        return [x for x in events if Capability.has(x)]

    def _wakeup(self, *args, **kwargs):
        """
        Not needed, the MCU never sleeps...
        """

    def _generic_response_handler(self, result, prompt, command=None):
        """
        Generic response handler to pass results through unmodified.
        @param result - result
        @param prompt - prompt
        @command - Command which generated the result
        @return result
        """
        return result

    def _set_params(self, *args, **kwargs):
        """
        This instrument has no params
        @throws InstrumentParameterException
        """
        self._verify_not_readonly(*args, **kwargs)
        params_to_set = args[0]
        startup = False
        if len(args) > 1:
            startup = args[1]
        old_config = self._param_dict.get_all()

        # check if in range
        constraints = ParameterConstraint.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params_to_set.iteritems():
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                try:
                    value = var_type(val)
                except ValueError:
                    raise InstrumentParameterException(
                        'Unable to verify type - parameter: %s value: %s' %
                        (key, val))
                if val < minimum or val > maximum:
                    raise InstrumentParameterException(
                        'Value out of range - parameter: %s value: %s min: %s max: %s'
                        % (key, val, minimum, maximum))

        # all constraints met or no constraints exist, set the values
        for key, val in params_to_set.iteritems():
            if key in old_config:
                self._param_dict.set_value(key, val)
            else:
                raise InstrumentParameterException(
                    'Attempted to set unknown parameter: %s value: %s' %
                    (key, val))
        new_config = self._param_dict.get_all()

        # If we changed anything, raise a CONFIG_CHANGE event
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _reset_mcu(self):
        """
        Reset the MCU via the watchdog timer
        """
        try:
            self.resetting = True
            # set the watchdog timer
            self._do_cmd_resp(InstrumentCommand.SET_WATCHDOG,
                              expected_prompt=Prompt.OK,
                              timeout=60)
            # try to put the MCU in standby, if successful watchdog will reset MCU
            result = self._do_cmd_resp(
                InstrumentCommand.STANDBY,
                expected_prompt=[Prompt.ONLINE, Prompt.IN_SEQUENCE],
                timeout=60)
            # MCU was in sequence, abort it and then go standby to reset MCU
            if result == Prompt.IN_SEQUENCE:
                self._do_cmd_resp(InstrumentCommand.ABORT,
                                  expected_prompt=Prompt.ABORTED,
                                  timeout=60)
                self._do_cmd_resp(InstrumentCommand.STANDBY,
                                  expected_prompt=Prompt.ONLINE,
                                  timeout=60)
            # MCU expects a BEAT after reset, send it
            self._do_cmd_resp(InstrumentCommand.BEAT,
                              expected_prompt=Prompt.BEAT)
            # set the MINUTE value
            self._do_cmd_resp(InstrumentCommand.SET_MINUTE,
                              expected_prompt=Prompt.SET_MINUTE)
            # This should actually put us in standby
            self._do_cmd_resp(InstrumentCommand.STANDBY,
                              expected_prompt=Prompt.STANDBY,
                              timeout=60)
        finally:
            self.resetting = False

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @return_value (next_state, result)
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.  Break out of any currently running sequence and return the MCU to STANDBY
        """
        self._init_params()

        try:
            self._reset_mcu()
        except InstrumentTimeoutException:
            # something else is wrong, pass the buck to the operator
            self._param_dict.set_value(
                Parameter.ERROR_REASON,
                'Timeout communicating with instrument.')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
            self._async_raise_fsm_event(ProtocolEvent.ERROR)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        This driver has no parameters, return an empty dict.
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        """
        self._set_params(*args, **kwargs)
        return None, None

    def _handler_command_start_direct(self):
        """
        Start direct access
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS,
                                             None)

    def _handler_command_start1(self):
        """
        Send the start1 command and move to the start1 state
        @return next_state, (next_agent_state, result)
        """
        self._reset_mcu()
        return ProtocolState.START1, (ResourceAgentState.BUSY,
                                      self._do_cmd_resp(
                                          InstrumentCommand.START1))

    def _handler_command_nafreg(self):
        """
        Send the nafreg command and move to the nafreg state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.REGEN, (ResourceAgentState.BUSY,
                                     self._do_cmd_resp(
                                         InstrumentCommand.NAFREG))

    def _handler_command_ionreg(self):
        """
        Send the ionreg command and move to the ionreg state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.REGEN, (ResourceAgentState.BUSY,
                                     self._do_cmd_resp(
                                         InstrumentCommand.IONREG))

    def _handler_command_poweroff(self):
        """
        Send the ionreg command and move to the ionreg state
        @return next_state, (next_agent_state, result)
        """
        return None, (None, self._do_cmd_resp(InstrumentCommand.POWEROFF))

    ########################################################################
    # START1 handlers.
    ########################################################################

    def _handler_start1_complete(self):
        """
        Start1 sequence complete, move to waiting_turbo
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.WAITING_TURBO, (ResourceAgentState.IDLE, None)

    ########################################################################
    # WAITING_TURBO handlers.
    ########################################################################

    def _handler_waiting_turbo_start2(self):
        """
        Turbo is at speed, send start2 and move to start2 state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.START2, (ResourceAgentState.BUSY,
                                      self._do_cmd_resp(
                                          InstrumentCommand.START2))

    ########################################################################
    # START2 handlers.
    ########################################################################

    def _handler_start2_complete(self):
        """
        Start2 complete, move to waiting_rga state
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.WAITING_RGA, (ResourceAgentState.BUSY, None)

    ########################################################################
    # WAITING_RGA handlers.
    ########################################################################

    def _handler_waiting_rga_sample(self):
        """
        RGA configuration/startup complete, send start sample and move to sample state
        @return next_state, (next_agent_state, result)
        """
        result = self._do_cmd_resp(InstrumentCommand.SAMPLE)
        self._do_cmd_resp(InstrumentCommand.SET_TELEGRAM_INTERVAL)
        return ProtocolState.SAMPLE, (ResourceAgentState.BUSY, result)

    def _handler_waiting_rga_cal(self):
        """
        RGA configuration/startup complete, send start cal and move to cal state
        @return next_state, (next_agent_state, result)
        """
        result = self._do_cmd_resp(InstrumentCommand.CAL)
        self._do_cmd_resp(InstrumentCommand.SET_TELEGRAM_INTERVAL)
        return ProtocolState.CALIBRATE, (ResourceAgentState.BUSY, result)

    ########################################################################
    # SAMPLE handlers.
    ########################################################################

    def _handler_sample_complete(self):
        """
        Sample complete, move to the stopping state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.STOPPING, (ResourceAgentState.BUSY, None)

    ########################################################################
    # CALIBRATE handlers.
    ########################################################################

    def _handler_cal_complete(self):
        """
        Cal complete, move to the stopping state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.STOPPING, (ResourceAgentState.BUSY, None)

    ########################################################################
    # ERROR handler. Handle in all states.
    ########################################################################

    def _handler_error(self):
        """
        Error detected, move to error state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.ERROR, (ResourceAgentState.BUSY, None)

    def _handler_stop(self):
        """
        Return to COMMAND
        """
        if self._param_dict.get(Parameter.ERROR_REASON):
            self._param_dict.set_value(Parameter.ERROR_REASON, '')
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    def _handler_error_standby(self):
        """
        Move instrument to STANDBY, stay in error state
        """
        self._reset_mcu()

    ########################################################################
    # GENERIC handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler
        """

    ########################################################################
    # 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):
        """
        Pass direct access commands through to the instrument
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)
        return None, (None, None)
Beispiel #36
0
class Protocol(InstrumentProtocol):
    def __init__(self, driver_event):
        super(Protocol, self).__init__(driver_event)
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        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.GET, self._handler_get),
                (ProtocolEvent.SET, self._handler_set),
                (ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample),
            ),
            ProtocolState.AUTOSAMPLE: (
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_autosample_exit),
                (ProtocolEvent.GET, self._handler_get),
                (ProtocolEvent.FLUSH, self._flush),
                (ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample),
            ),
            ProtocolState.STOPPING: (
                (ProtocolEvent.ENTER, self._handler_stopping_enter),
                (ProtocolEvent.EXIT, self._handler_stopping_exit),
                (ProtocolEvent.FLUSH, self._flush),
            ),
            ProtocolState.WRITE_ERROR: (
                (ProtocolEvent.ENTER, self._handler_write_error_enter),
                (ProtocolEvent.EXIT, self._handler_write_error_exit),
                (ProtocolEvent.CLEAR_WRITE_ERROR, self._handler_clear_write_error),
            ),
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # Build dictionaries for driver schema
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)
        self._logs = {}
        self._filled_logs = []
        self._pickle_cache = []

        # persistent store, cannot initialize until startup config has been applied
        # since we need the address for postgres
        self._persistent_store = None

        # lock for flush actions to prevent writing or altering the data files
        # during flush
        self._lock = Lock()
        self._pktid = 0

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE, display_name="Start Autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE, display_name="Stop Autosample")
        self._cmd_dict.add(Capability.GET, display_name="Get")
        self._cmd_dict.add(Capability.SET, display_name="Set")
        self._cmd_dict.add(Capability.DISCOVER, display_name="Discover")
        self._cmd_dict.add(Capability.CLEAR_WRITE_ERROR, display_name="Clear Write Error")

    def _build_param_dict(self):
        self._param_dict.add(
            Parameter.REFDES,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name="Reference Designator",
            description="Reference Designator for this driver",
            type=ParameterDictType.STRING,
        )
        self._param_dict.add(
            Parameter.SOURCE_REGEX,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name="Source Filter Regex",
            description="Filter sources to be processed from the ORB",
            type=ParameterDictType.STRING,
            value_description="Regular expression",
        )
        self._param_dict.add(
            Parameter.FLUSH_INTERVAL,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            display_name="Flush Interval",
            description="Interval after which all records are flushed to disk",
            type=ParameterDictType.INT,
            value_description="Interval, in seconds",
            units=Units.SECOND,
        )
        self._param_dict.add(
            Parameter.DB_ADDR,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value="localhost",
            display_name="Database Address",
            description="Postgres database IP address or hostname",
            type=ParameterDictType.STRING,
            value_description="IP address or hostname",
        )
        self._param_dict.add(
            Parameter.DB_PORT,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value=5432,
            display_name="Database Port",
            description="Postgres database port number",
            type=ParameterDictType.INT,
            value_description="Integer port number (default 5432)",
        )
        self._param_dict.add(
            Parameter.FILE_LOCATION,
            "NA",
            str,
            str,
            visibility=ParameterDictVisibility.IMMUTABLE,
            startup_param=True,
            default_value="./antelope_data",
            display_name="File Location",
            description="Root file path of the packet data files",
            type=ParameterDictType.STRING,
            value_description="String representing the packet data root file path",
        )

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_persistent_dict(self):
        name = "antelope"
        refdes = self._param_dict.get(Parameter.REFDES)
        host = self._param_dict.get(Parameter.DB_ADDR)
        port = self._param_dict.get(Parameter.DB_PORT)

        self._persistent_store = PersistentStoreDict(name, refdes, host=host, port=port)
        if "pktid" not in self._persistent_store:
            self._persistent_store["pktid"] = ORBOLDEST

    def _handler_set(self, *args, **kwargs):
        pass

    def _update_params(self, *args, **kwargs):
        pass

    def _set_params(self, *args, **kwargs):
        """
        Set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException("Set command requires a parameter dict.")

        self._verify_not_readonly(*args, **kwargs)

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Set the base directory for the packet data file location.
        PacketLog.base_dir = self._param_dict.get(Parameter.FILE_LOCATION)

    def _flush(self):
        log.info("flush")
        particles = []
        with self._lock:
            log.info("got lock")

            # On the last flush, close all the bins.
            last_flush = self.get_current_state() == ProtocolState.STOPPING
            if last_flush:
                self._filled_logs.extend(self._logs.values())
                self._logs = {}

            for _log in self._logs.itervalues():
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (ResourceAgentState.STOPPED, None)

                particles.append(AntelopeMetadataParticle(_log, preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP))

            for _log in self._filled_logs:
                try:
                    _log.flush()
                except InstrumentProtocolException as ex:
                    # Ensure the current logs are clear to prevent residual data from being flushed.
                    self._driver_event(DriverAsyncEvent.ERROR, ex)
                    self._logs = {}
                    self._filled_logs = []
                    return ProtocolState.WRITE_ERROR, (ResourceAgentState.STOPPED, None)

                particles.append(AntelopeMetadataParticle(_log, preferred_timestamp=DataParticleKey.INTERNAL_TIMESTAMP))
                _log.data = []

            self._filled_logs = []
            log.info("updating persistent store")
            self._persistent_store["pktid"] = self._pktid

        for particle in particles:
            self._driver_event(DriverAsyncEvent.SAMPLE, particle.generate())

        if last_flush:
            self.stop_scheduled_job(ScheduledJob.FLUSH)
            return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

        return None, (None, None)

    # noinspection PyProtectedMember
    def _orbstart(self):
        self._connection._command_port_agent("orbselect %s" % self._param_dict.get(Parameter.SOURCE_REGEX))
        self._connection._command_port_agent("orbseek %s" % self._persistent_store["pktid"])
        self._connection._command_port_agent("orbstart")

    # noinspection PyProtectedMember
    def _orbstop(self):
        self._connection._command_port_agent("orbstop")

    def stop_scheduled_job(self, schedule_job):
        """
        Remove the scheduled job
        @param schedule_job scheduling job.
        """
        if self._scheduler is not None:
            try:
                self._remove_scheduler(schedule_job)
            except KeyError:
                log.warn("_remove_scheduler could not find %s", schedule_job)

    def start_scheduled_job(self, param, schedule_job, protocol_event):
        """
        Add a scheduled job
        """
        self.stop_scheduled_job(schedule_job)
        val = self._param_dict.get(param)

        try:
            seconds = int(val)
        except ValueError:
            raise InstrumentParameterException("Bad interval. Cannot parse %r as integer" % val)

        if seconds > 0:
            config = {
                DriverConfigKey.SCHEDULER: {
                    schedule_job: {
                        DriverSchedulerConfigKey.TRIGGER: {
                            DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                            DriverSchedulerConfigKey.SECONDS: seconds,
                        }
                    }
                }
            }
            self.set_init_params(config)
            self._add_scheduler_event(schedule_job, protocol_event)

    def got_data(self, port_agent_packet):
        data_length = port_agent_packet.get_data_length()
        data_type = port_agent_packet.get_header_type()

        if data_type == PortAgentPacket.PICKLED_FROM_INSTRUMENT:
            self._pickle_cache.append(port_agent_packet.get_data())
            # this is the max size (65535) minus the header size (16)
            # any packet of this length will be followed by one or more packets
            # with additional data. Keep accumulating packets until we have
            # the complete data, then unpickle.
            if data_length != 65519:
                data = pickle.loads("".join(self._pickle_cache))
                self._pickle_cache = []
                self._bin_data(data)
        else:
            raise InstrumentProtocolException("Received unpickled data from port agent")

    def got_raw(self, port_agent_packet):
        pass

    def _get_bin(self, packet):
        rate_map = {
            1: 86400 * 7,  # 1 week
            8: 86400,  # 1 day
            40: 86400,  # 1 day
            200: 86400,  # 1 day
            64000: 60 * 5,  # 5 minutes
            256000: 60,  # 1 minute
        }
        start_time = packet["time"]
        rate = packet["samprate"]
        bin_size = rate_map.get(rate, 60)
        bin_value = int(start_time / bin_size)
        bin_start = bin_value * bin_size
        bin_end = (bin_value + 1) * bin_size

        return bin_start, bin_end

    def _bin_data(self, packet):
        key = "%s.%s.%s.%s" % (packet["net"], packet.get("location", ""), packet.get("sta", ""), packet["chan"])
        start, end = self._get_bin(packet)

        with self._lock:
            self._pktid = packet["pktid"]

            if key not in self._logs:
                self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))

            try:
                while True:
                    packet = self._logs[key].add_packet(packet)
                    if packet is None:
                        break
                    # residual, we need a new bin
                    # log is complete, move to holding list until next flush
                    self._filled_logs.append(self._logs[key])
                    del self._logs[key]
                    # create the new log...
                    start, end = self._get_bin(packet)
                    self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))

            except GapException:
                # non-contiguous data detected, close this log and open a new one
                self._filled_logs.append(self._logs[key])
                del self._logs[key]
                # create the new log
                self._logs[key] = PacketLog.from_packet(packet, end, self._param_dict.get(Parameter.REFDES))
                self._logs[key].add_packet(packet)

    ########################################################################
    # 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.
        """

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; always COMMAND.
        @return protocol_state, agent_state
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # COMMAND handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        self._init_params()
        # We can't build the persistent dict until parameters are applied, so build it here
        if self._persistent_store is None:
            self._build_persistent_dict()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_exit(self, *args, **kwargs):
        """
        Exit command state.
        """

    def _handler_command_start_autosample(self, *args, **kwargs):
        """
        Switch into autosample mode.
        @return next_state, (next_agent_state, result) if successful.
        """
        result = None

        # Ensure the current logs are clear to prevent residual data from being flushed.
        self._logs = {}
        self._filled_logs = []

        self._orbstart()
        next_state = ProtocolState.AUTOSAMPLE
        next_agent_state = ResourceAgentState.STREAMING

        return next_state, (next_agent_state, result)

    ######################################################
    # AUTOSAMPLE handlers
    ######################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self.start_scheduled_job(Parameter.FLUSH_INTERVAL, ScheduledJob.FLUSH, ProtocolEvent.FLUSH)
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_exit(self, *args, **kwargs):
        """
        Exit autosample state.
        """
        self._orbstop()

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample and switch back to command mode.
        @return  next_state, (next_agent_state, result) if successful.
        """
        self._orbstop()

        result = None
        next_state = ProtocolState.STOPPING
        next_agent_state = None

        return next_state, (next_agent_state, result)

    ######################################################
    # STOPPING handlers
    ######################################################

    def _handler_stopping_enter(self, *args, **kwargs):
        """
        Enter stopping state.
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_stopping_exit(self, *args, **kwargs):
        """
        Exit stopping state.
        Stop the scheduled flush job and schedule flush one more time and
        indicate that it is the last flush before stopping auto sampling.
        """
        pass

    ######################################################
    # WRITE_ERROR handlers
    ######################################################

    def _handler_write_error_enter(self, *args, **kwargs):
        """
        Enter write error state.
        """
        self.stop_scheduled_job(ScheduledJob.FLUSH)

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_write_error_exit(self, *args, **kwargs):
        """
        Exit write error state.
        """
        pass

    def _handler_clear_write_error(self, *args, **kwargs):
        """
        Clear the WRITE_ERROR state by transitioning to the COMMAND state.
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)
Beispiel #37
0
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):
        """
Beispiel #38
0
class Protocol(Pco2wProtocol):
    """
    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.
        """

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(
            ProtocolState, ProtocolEvent,
            ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Construct protocol superclass.
        Pco2wProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.

        self._protocol_fsm.add_handler(
            ProtocolState.COMMAND, ProtocolEvent.RUN_EXTERNAL_PUMP,
            self._handler_command_run_external_pump)

        # this state would be entered whenever a RUN_EXTERNAL_PUMP event
        # occurred while in the COMMAND state
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.ENTER,
            self._execution_state_enter)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.EXIT,
            self._execution_state_exit)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.EXECUTE,
            self._handler_run_external_pump_execute)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.SUCCESS,
            self._execution_success_to_command_state)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.TIMEOUT,
            self._execution_timeout_to_command_state)
        ## Events to queue - intended for schedulable events occurring when a sample is being taken
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.ACQUIRE_STATUS,
            self._handler_queue_acquire_status)

        # Add build handlers for device commands.
        ### primarily defined in base class
        self._add_build_handler(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1, self._build_simple_command)
        # Add response handlers for device commands.
        ### primarily defined in base class
        self._add_response_handler(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1, self._parse_response_sample_dev1)

        # Add sample handlers

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # build the chunker
        self._chunker = StringChunker(Protocol.sieve_function)

        self._engineering_parameters.append(Parameter.EXTERNAL_PUMP_DELAY)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """

        return [x for x in events if Capability.has(x)]

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_run_external_pump(self):
        """
        Run external pump
        """

        next_state = ProtocolState.RUN_EXTERNAL_PUMP
        next_agent_state = ResourceAgentState.BUSY
        result = None

        return next_state, (next_agent_state, result)

    ########################################################################
    # Run external pump handlers.
    ########################################################################

    def _handler_run_external_pump_execute(self, *args, **kwargs):
        """
        Execute run external pump (dev1) command
        """

        try:

            self._take_dev1_sample()

            log.debug('Protocol._handler_run_external_pump_execute(): SUCCESS')

            self._async_raise_fsm_event(ProtocolEvent.SUCCESS)

        except InstrumentTimeoutException:

            log.error('Protocol._handler_run_external_pump_execute(): TIMEOUT')

            self._async_raise_fsm_event(ProtocolEvent.TIMEOUT)

        return None, None

    ########################################################################
    # Response handlers.
    ########################################################################

    def _parse_response_sample_dev1(self, response, prompt):
        """
        Parse response to take dev1 sample from instrument
        """

    def _take_dev1_sample(self):
        """
        Run external pump and wait for dev1 sample
        """

        log.debug('Protocol._take_dev1_sample(): Take Dev1 Sample START')

        start_time = time.time()

        dev1_timeout = self._param_dict.get(Parameter.EXTERNAL_PUMP_SETTINGS)

        log.debug('Protocol._take_dev1_sample(): Dev1 Timeout = %s', dev1_timeout)

        ## An exception is raised if timeout is hit.
        self._do_cmd_resp(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1,
                          timeout=dev1_timeout,
                          response_regex=PCO2WB_DEV1_SAMPLE_REGEX_MATCHER)

        sample_time = time.time() - start_time

        log.debug('Protocol._take_dev1_sample(): Dev1 Sample took %s to FINISH', sample_time)

    def _pre_sample_processing(self):
        """
        Run external pump and wait for equilibrium
        """

        self._take_dev1_sample()

        external_pump_delay = self._param_dict.get(Parameter.EXTERNAL_PUMP_DELAY)

        log.debug('Protocol._pre_sample_processing(): Delaying for %d seconds', external_pump_delay)

        time.sleep(external_pump_delay)

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        :param raw_data: data to filter
        """

        return_list = []

        sieve_matchers = [SAMI_REGULAR_STATUS_REGEX_MATCHER,
                          PCO2W_SAMPLE_REGEX_MATCHER,
                          PCO2WB_DEV1_SAMPLE_REGEX_MATCHER,
                          PCO2WB_CONFIGURATION_REGEX_MATCHER,
                          SAMI_ERROR_REGEX_MATCHER]

        for matcher in sieve_matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker. Pass it to
        extract_sample with the appropriate particle objects and REGEXes.
        """

        if any([
                self._extract_sample(SamiRegularStatusDataParticle, SAMI_REGULAR_STATUS_REGEX_MATCHER,
                                     chunk, timestamp),
                self._extract_sample(Pco2wConfigurationDataParticle, PCO2WB_CONFIGURATION_REGEX_MATCHER,
                                     chunk, timestamp)]):
            return

        dev1_sample = self._extract_sample(Pco2wbDev1SampleDataParticle, PCO2WB_DEV1_SAMPLE_REGEX_MATCHER, chunk,
                                           timestamp)
        sami_sample = self._extract_sample(Pco2wSamiSampleDataParticle, PCO2W_SAMPLE_REGEX_MATCHER_NORMAL, chunk,
                                           timestamp)
        if sami_sample is None:
            sami_sample = self._extract_sample(Pco2wSamiSampleCalibrationDataParticle, PCO2W_SAMPLE_REGEX_MATCHER_CAL,
                                               chunk, timestamp)

        log.debug('Protocol._got_chunk(): get_current_state() == %s', self.get_current_state())

        if sami_sample:
            self._verify_checksum(chunk, PCO2W_SAMPLE_REGEX_MATCHER)
        elif dev1_sample:
            self._verify_checksum(chunk, PCO2WB_DEV1_SAMPLE_REGEX_MATCHER)

    ########################################################################
    # Build Command, Driver and Parameter dictionaries
    ########################################################################

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """

        Pco2wProtocol._build_command_dict(self)

        self._cmd_dict.add(Capability.RUN_EXTERNAL_PUMP, display_name="Run External Pump")

    def _build_param_dict(self):
        """
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        Pco2wProtocol._build_param_dict(self)

        ### example configuration string
        # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \
        #                       '000000000D000000000D000000000D07' + \
        #                       '1020FF54181C01003814' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '0000000000000000000000000000' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE
        #
        ###

        configuration_string_regex = self._get_configuration_string_regex()

        self._param_dict.add(Parameter.MODE_BITS, configuration_string_regex,
                             lambda match: int(match.group(4), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x0A,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Mode Bits')

        self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, configuration_string_regex,
                             lambda match: int(match.group(8), 16),
                             lambda x: self._int_to_hexstring(x, 6),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x000E10,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Sample Interval')

        self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, configuration_string_regex,
                             lambda match: int(match.group(9), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x01,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Driver Version')

        self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, configuration_string_regex,
                             lambda match: int(match.group(10), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x0B,
                             visibility=ParameterDictVisibility.READ_ONLY,
                             display_name='Device 1 Parameter Pointer')

        self._param_dict.add(Parameter.EXTERNAL_PUMP_SETTINGS, configuration_string_regex,
                             lambda match: int(match.group(30), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x1E,
                             visibility=ParameterDictVisibility.READ_WRITE,
                             display_name='External Pump Settings')

        ## Engineering parameter to set delay after running external pump to take a sample, set as startup parameter
        ##   because it is configurable by the user and should be reapplied on application of startup parameters.
        self._param_dict.add(Parameter.EXTERNAL_PUMP_DELAY, r'External pump delay = ([0-9]+)',
                             lambda match: match.group(1),
                             lambda x: int(x),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=False,
                             default_value=360,
                             visibility=ParameterDictVisibility.READ_WRITE,
                             display_name='External Pump Delay')

    ########################################################################
    # Overridden base class methods
    ########################################################################

    def _get_specific_configuration_string_parameters(self):
        """
        Overridden by device specific subclasses.
        """

        # An ordered list of parameters, can not use unordered dict
        # PCO2W driver extends the base class (SamiParameter)
        parameter_list = [Parameter.START_TIME_FROM_LAUNCH,
                          Parameter.STOP_TIME_FROM_START,
                          Parameter.MODE_BITS,
                          Parameter.SAMI_SAMPLE_INTERVAL,
                          Parameter.SAMI_DRIVER_VERSION,
                          Parameter.SAMI_PARAMS_POINTER,
                          Parameter.DEVICE1_SAMPLE_INTERVAL,
                          Parameter.DEVICE1_DRIVER_VERSION,
                          Parameter.DEVICE1_PARAMS_POINTER,
                          Parameter.DEVICE2_SAMPLE_INTERVAL,
                          Parameter.DEVICE2_DRIVER_VERSION,
                          Parameter.DEVICE2_PARAMS_POINTER,
                          Parameter.DEVICE3_SAMPLE_INTERVAL,
                          Parameter.DEVICE3_DRIVER_VERSION,
                          Parameter.DEVICE3_PARAMS_POINTER,
                          Parameter.PRESTART_SAMPLE_INTERVAL,
                          Parameter.PRESTART_DRIVER_VERSION,
                          Parameter.PRESTART_PARAMS_POINTER,
                          Parameter.GLOBAL_CONFIGURATION,
                          Parameter.PUMP_PULSE,
                          Parameter.PUMP_DURATION,
                          Parameter.SAMPLES_PER_MEASUREMENT,
                          Parameter.CYCLES_BETWEEN_BLANKS,
                          Parameter.NUMBER_REAGENT_CYCLES,
                          Parameter.NUMBER_BLANK_CYCLES,
                          Parameter.FLUSH_PUMP_INTERVAL,
                          Parameter.BIT_SWITCHES,
                          Parameter.NUMBER_EXTRA_PUMP_CYCLES,
                          Parameter.EXTERNAL_PUMP_SETTINGS]

        return parameter_list

    def _get_configuration_string_regex(self):
        """
        Get configuration string regex.
        @retval configuration string regex.
        """
        return PCO2WB_CONFIGURATION_REGEX

    def _get_configuration_string_regex_matcher(self):
        """
        Get config string regex matcher.
        @retval configuration string regex matcher
        """
        return PCO2WB_CONFIGURATION_REGEX_MATCHER
Beispiel #39
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.AUTOSAMPLE: [
                (ProtocolEvent.ENTER, self._handler_autosample_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_AUTOSAMPLE,
                 self._handler_autosample_stop_autosample),
                (ProtocolEvent.START_LEVELING, self._handler_start_leveling),
                (ProtocolEvent.STOP_LEVELING, self._handler_stop_leveling),
                (ProtocolEvent.NANO_TIME_SYNC, self._handler_time_sync),
                (ProtocolEvent.START_HEATER, self._handler_start_heater),
                (ProtocolEvent.STOP_HEATER, self._handler_stop_heater),
                (ProtocolEvent.LEVELING_TIMEOUT,
                 self._handler_leveling_timeout),
                (ProtocolEvent.HEATER_TIMEOUT, self._handler_heater_timeout),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.START_AUTOSAMPLE,
                 self._handler_command_start_autosample),
                (ProtocolEvent.START_LEVELING, self._handler_start_leveling),
                (ProtocolEvent.STOP_LEVELING, self._handler_stop_leveling),
                (ProtocolEvent.START_DIRECT,
                 self._handler_command_start_direct),
                (ProtocolEvent.NANO_TIME_SYNC, self._handler_time_sync),
                (ProtocolEvent.START_HEATER, self._handler_start_heater),
                (ProtocolEvent.STOP_HEATER, self._handler_stop_heater),
                (ProtocolEvent.LEVELING_TIMEOUT,
                 self._handler_leveling_timeout),
                (ProtocolEvent.HEATER_TIMEOUT, self._handler_heater_timeout),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_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)

        # Construct the metadata dictionaries
        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        # Add build handlers for device commands.
        for command in InstrumentCommand.list():
            if command in [
                    InstrumentCommand.NANO_SET_RATE, InstrumentCommand.HEAT
            ]:
                self._add_build_handler(command,
                                        self._build_command_with_value)
            else:
                self._add_build_handler(command, self._build_simple_command)

        # # Add response handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_response_handler(command, self._generic_response_handler)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        # create chunker
        self._chunker = StringChunker(Protocol.sieve_function)

        self._last_data_timestamp = 0
        self.has_pps = True

        # set up scheduled event handling
        self.initialize_scheduler()
        self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS,
                                  ProtocolEvent.ACQUIRE_STATUS)
        self._add_scheduler_event(ScheduledJob.NANO_TIME_SYNC,
                                  ProtocolEvent.NANO_TIME_SYNC)

    @staticmethod
    def sieve_function(raw_data):
        """
        Sort data in the chunker...
        @param raw_data: Data to be searched for samples
        @return: list of (start,end) tuples
        """
        matchers = []
        return_list = []

        matchers.append(particles.HeatSampleParticle.regex_compiled())
        matchers.append(particles.IrisSampleParticle.regex_compiled())
        matchers.append(particles.NanoSampleParticle.regex_compiled())
        matchers.append(particles.LilySampleParticle.regex_compiled())
        matchers.append(particles.LilyLevelingParticle.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, ts):
        """
        Process chunk output by the chunker.  Generate samples and (possibly) react
        @param chunk: data
        @param ts: ntp timestamp
        @return sample
        @throws InstrumentProtocolException
        """
        possible_particles = [
            (particles.LilySampleParticle, self._check_for_autolevel),
            (particles.LilyLevelingParticle, self._check_completed_leveling),
            (particles.HeatSampleParticle, None),
            (particles.IrisSampleParticle, None),
            (particles.NanoSampleParticle, self._check_pps_sync),
        ]

        for particle_type, func in possible_particles:
            sample = self._extract_sample(particle_type,
                                          particle_type.regex_compiled(),
                                          chunk, ts)
            if sample:
                if func:
                    func(sample)
                return sample

        raise InstrumentProtocolException(
            u'unhandled chunk received by _got_chunk: [{0!r:s}]'.format(chunk))

    def _extract_sample(self,
                        particle_class,
                        regex,
                        line,
                        timestamp,
                        publish=True):
        """
        Overridden to set the quality flag for LILY particles that are out of range.
        @param particle_class: Class type for particle
        @param regex: regular expression to verify data
        @param line: data
        @param timestamp: ntp timestamp
        @param publish: boolean to indicate if sample should be published
        @return: extracted sample
        """
        if regex.match(line):
            if particle_class == particles.LilySampleParticle and self._param_dict.get(
                    Parameter.LEVELING_FAILED):
                particle = particle_class(
                    line,
                    port_timestamp=timestamp,
                    quality_flag=DataParticleValue.OUT_OF_RANGE)
            else:
                particle = particle_class(line, port_timestamp=timestamp)
            parsed_sample = particle.generate()

            if publish and self._driver_event:
                self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample)

            return parsed_sample

    def _filter_capabilities(self, events):
        """
        Filter a list of events to only include valid capabilities
        @param events: list of events to be filtered
        @return: list of filtered events
        """
        return [x for x in events if Capability.has(x)]

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.START_AUTOSAMPLE,
                           display_name="Start autosample")
        self._cmd_dict.add(Capability.STOP_AUTOSAMPLE,
                           display_name="Stop autosample")
        self._cmd_dict.add(Capability.ACQUIRE_STATUS,
                           display_name="Acquire instrument status")
        self._cmd_dict.add(Capability.START_LEVELING,
                           display_name="Start the LILY leveling sequence")
        self._cmd_dict.add(Capability.STOP_LEVELING,
                           display_name="Stop the LILY leveling sequence")
        self._cmd_dict.add(Capability.START_HEATER,
                           display_name="Start the heater")
        self._cmd_dict.add(Capability.STOP_HEATER,
                           display_name="Stop the heater")

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """
        my_regex = 'Not used'
        ro, rw = ParameterDictVisibility.READ_ONLY, ParameterDictVisibility.READ_WRITE
        _bool, _float, _int = ParameterDictType.BOOL, ParameterDictType.FLOAT, ParameterDictType.INT

        parameters = {
            Parameter.AUTO_RELEVEL: {
                'type': _bool,
                'display_name': 'Automatic Releveling Enabled',
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.XTILT_TRIGGER: {
                'type': _float,
                'display_name': 'X-tilt Releveling Trigger',
                'units': Prefixes.MICRO + Units.RADIAN,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.YTILT_TRIGGER: {
                'type': _float,
                'display_name': 'Y-tilt Releveling Trigger',
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.LEVELING_TIMEOUT: {
                'type': _int,
                'display_name': 'LILY Leveling Timeout',
                'units': Units.SECOND,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.HEAT_DURATION: {
                'type': _int,
                'display_name': 'Heater Run Time Duration',
                'units': Units.HOUR,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.OUTPUT_RATE: {
                'type': _int,
                'display_name': 'NANO Output Rate',
                'units': Units.HERTZ,
                'visibility': rw,
                'startup_param': True,
            },
            Parameter.HEATER_ON: {
                'type': _bool,
                'display_name': 'Heater Running',
                'value': False,
                'visibility': ro,
            },
            Parameter.LILY_LEVELING: {
                'type': _bool,
                'display_name': 'Lily Leveling',
                'value': False,
                'visibility': ro,
            },
            Parameter.LEVELING_FAILED: {
                'type': _bool,
                'display_name': 'LILY Leveling Failed',
                'value': False,
                'visibility': ro,
            },
        }
        for param in parameters:
            self._param_dict.add(param, my_regex, None, None,
                                 **parameters[param])

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _build_command_with_value(self, cmd, value):
        """
        Build a simple command with one value specified
        @param cmd: instrument command
        @param value: value to be sent
        @return: command string
        """
        return '%s%d%s' % (cmd, value, NEWLINE)

    def _verify_set_values(self, params):
        """
        Verify supplied values are in range, if applicable
        @param params: Dictionary of Parameter:value pairs to be verified
        @throws InstrumentParameterException
        """
        constraints = ParameterConstraint.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params.iteritems():
            # verify this parameter exists
            if not Parameter.has(key):
                raise InstrumentParameterException(
                    'Received invalid parameter in SET: %s' % key)
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                constraint_string = 'Parameter: %s Value: %s Type: %s Minimum: %s Maximum: %s' % \
                                    (key, val, var_type, minimum, maximum)
                log.debug('SET CONSTRAINT: %s', constraint_string)
                # check bool values are actual booleans
                if var_type == bool:
                    if val not in [True, False]:
                        raise InstrumentParameterException(
                            'Non-boolean value!: %s' % constraint_string)
                # else, check if we can cast to the correct type
                else:
                    try:
                        var_type(val)
                    except ValueError:
                        raise InstrumentParameterException(
                            'Type mismatch: %s' % constraint_string)
                    # now, verify we are within min/max
                    if val < minimum or val > maximum:
                        raise InstrumentParameterException('Out of range: %s' %
                                                           constraint_string)

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        @param args: arglist, should contain a dictionary of parameters/values to be set
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        self._verify_set_values(params)
        self._verify_not_readonly(*args, **kwargs)

        # if setting the output rate, get the current rate from the instrument first...
        if Parameter.OUTPUT_RATE in params:
            self._update_params()

        old_config = self._param_dict.get_config()

        # all constraints met or no constraints exist, set the values
        for key, value in params.iteritems():
            self._param_dict.set_value(key, value)

        new_config = self._param_dict.get_config()

        if not old_config == new_config:
            log.debug('Config change: %r %r', old_config, new_config)
            if old_config[Parameter.OUTPUT_RATE] is not None:
                if int(old_config[Parameter.OUTPUT_RATE]) != int(
                        new_config[Parameter.OUTPUT_RATE]):
                    self._do_cmd_no_resp(
                        InstrumentCommand.NANO_SET_RATE,
                        int(new_config[Parameter.OUTPUT_RATE]))
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _update_params(self, *args, **kwargs):
        """
        Update the param dictionary based on instrument response
        """
        result, _ = self._do_cmd_resp(
            InstrumentCommand.NANO_DUMP1,
            response_regex=particles.NanoStatusParticle.regex_compiled())
        rate = int(re.search(r'NANO,\*TH:(\d+)', result).group(1))
        self._param_dict.set_value(Parameter.OUTPUT_RATE, rate)

    def _wakeup(self, timeout, delay=1):
        """
        Overriding _wakeup; does not apply to this instrument
        """

    def add_to_buffer(self, data):
        """
        Overriding base class to reduce logging due to NANO high data rate
        @param data: data to be added to buffers
        """
        # Update the line and prompt buffers.
        self._linebuf += data
        self._promptbuf += data
        self._last_data_timestamp = time.time()

        # If our buffer exceeds the max allowable size then drop the leading
        # characters on the floor.
        max_size = self._max_buffer_size()
        if len(self._linebuf) > max_size:
            self._linebuf = self._linebuf[max_size * -1:]

        # If our buffer exceeds the max allowable size then drop the leading
        # characters on the floor.
        if len(self._promptbuf) > max_size:
            self._promptbuf = self._linebuf[max_size * -1:]

    def _max_buffer_size(self):
        """
        Overriding base class to increase max buffer size
        @return int max_buffer_size
        """
        return MAX_BUFFER_SIZE

    def _remove_leveling_timeout(self):
        """
        Clean up the leveling timer
        """
        try:
            self._remove_scheduler(ScheduledJob.LEVELING_TIMEOUT)
        except KeyError:
            log.debug(
                'Unable to remove LEVELING_TIMEOUT scheduled job, job does not exist.'
            )

    def _schedule_leveling_timeout(self):
        """
        Set up a leveling timer to make sure we don't stay in leveling state forever if something goes wrong
        """
        self._remove_leveling_timeout()
        dt = datetime.datetime.now() + datetime.timedelta(
            seconds=self._param_dict.get(Parameter.LEVELING_TIMEOUT))
        job_name = ScheduledJob.LEVELING_TIMEOUT
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE:
                        TriggerType.ABSOLUTE,
                        DriverSchedulerConfigKey.DATE: dt
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.LEVELING_TIMEOUT,
                                  ProtocolEvent.LEVELING_TIMEOUT)

    def _remove_heater_timeout(self):
        """
        Clean up the heater timer
        """
        try:
            self._remove_scheduler(ScheduledJob.HEATER_TIMEOUT)
        except KeyError:
            log.debug(
                'Unable to remove HEATER_TIMEOUT scheduled job, job does not exist.'
            )

    def _schedule_heater_timeout(self):
        """
        Set up a timer to set HEATER_ON to false around the time the heater shuts off
        """
        self._remove_heater_timeout()
        dt = datetime.datetime.now() + datetime.timedelta(
            hours=self._param_dict.get(Parameter.HEAT_DURATION))
        job_name = ScheduledJob.HEATER_TIMEOUT
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE:
                        TriggerType.ABSOLUTE,
                        DriverSchedulerConfigKey.DATE: dt
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.HEATER_TIMEOUT,
                                  ProtocolEvent.HEATER_TIMEOUT)

    def _stop_autosample(self):
        """
        Stop autosample, leveling if in progress.
        """
        self.leveling = False
        self._do_cmd_no_resp(InstrumentCommand.NANO_OFF)
        self._do_cmd_resp(InstrumentCommand.LILY_STOP_LEVELING,
                          expected_prompt=Prompt.LILY_STOP_LEVELING)
        self._do_cmd_resp(InstrumentCommand.LILY_OFF,
                          expected_prompt=Prompt.LILY_OFF)
        self._do_cmd_resp(InstrumentCommand.IRIS_OFF,
                          expected_prompt=Prompt.IRIS_OFF)

    def _generic_response_handler(self, resp, prompt):
        """
        Pass through response handler
        @param resp: response
        @param prompt: prompt
        @return: (response, prompt)
        """
        return resp, prompt

    def _particle_to_dict(self, sample):
        """
        Convert a particle to a dictionary of value_id:value
        @param sample: particle to be parsed
        @return: dictionary representing the particle
        """
        sample_dict = {}
        values = sample.get(DataParticleKey.VALUES, [])
        for each in values:
            sample_dict[each[DataParticleKey.VALUE_ID]] = each[
                DataParticleKey.VALUE]
        return sample_dict

    def _check_for_autolevel(self, sample):
        """
        Check this sample, kick off a leveling event if out of range
        @param sample: sample to be checked
        """
        if self._param_dict.get(
                Parameter.AUTO_RELEVEL) and self.get_current_state(
                ) == ProtocolState.AUTOSAMPLE:
            # Find the current X and Y tilt values
            # If they exceed the trigger parameters, begin autolevel
            relevel = False
            sample = self._particle_to_dict(sample)
            x_tilt = abs(sample[particles.LilySampleParticleKey.X_TILT])
            y_tilt = abs(sample[particles.LilySampleParticleKey.Y_TILT])
            x_trig = int(self._param_dict.get(Parameter.XTILT_TRIGGER))
            y_trig = int(self._param_dict.get(Parameter.YTILT_TRIGGER))
            if x_tilt > x_trig or y_tilt > y_trig:
                self._async_raise_fsm_event(ProtocolEvent.START_LEVELING)

    def _failed_leveling(self, axis):
        """
        Handle a failed leveling event.  Set the failed flag, disable auto relevel and notify the operator
        @param axis: Axis which failed leveling
        """
        log.error('Detected leveling error in %s axis!', axis)
        # Read only parameter, must be set outside of handler
        self._param_dict.set_value(Parameter.LEVELING_FAILED, True)
        # Use the handler to disable auto relevel to raise a config change event if needed.
        self._handler_command_set({Parameter.AUTO_RELEVEL: False})
        raise InstrumentDataException(
            'LILY Leveling (%s) Failed.  Disabling auto relevel' % axis)

    def _check_completed_leveling(self, sample):
        """
        Check this sample if leveling is complete or failed
        @param sample: Sample to be checked
        """
        sample = self._particle_to_dict(sample)
        status = sample[particles.LilyLevelingParticleKey.STATUS]
        if status is not None:
            # Leveling status update received
            # If leveling complete, send STOP_LEVELING, set the _leveling_failed flag to False
            if 'Leveled' in status:
                if self._param_dict.get(Parameter.LEVELING_FAILED):
                    self._handler_command_set(
                        {Parameter.LEVELING_FAILED: False})
                self._async_raise_fsm_event(ProtocolEvent.STOP_LEVELING)
            # Leveling X failed!  Set the flag and raise an exception to notify the operator
            # and disable auto leveling. Let the instrument attempt to level
            # in the Y axis.
            elif 'X Axis out of range' in status:
                self._failed_leveling('X')
            # Leveling X failed!  Set the flag and raise an exception to notify the operator
            # and disable auto leveling. Send STOP_LEVELING
            elif 'Y Axis out of range' in status:
                self._async_raise_fsm_event(ProtocolEvent.STOP_LEVELING)
                self._failed_leveling('Y')

    def _check_pps_sync(self, sample):
        """
        Check if PPS sync status has changed.  Update driver flag and, if appropriate, trigger a time sync
        @param sample: sample to be checked
        """
        sample = self._particle_to_dict(sample)
        pps_sync = sample[particles.NanoSampleParticleKey.PPS_SYNC] == 'P'
        if pps_sync and not self.has_pps:
            # pps sync regained, sync the time
            self.has_pps = True
            self._async_raise_fsm_event(ProtocolEvent.NANO_TIME_SYNC)
        elif self.has_pps:
            self.has_pps = False

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Process discover event
        @return next_state, next_agent_state
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        self._init_params()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        # key off the initialization flag to determine if we should sync the time
        if self._init_type == InitializationType.STARTUP:
            self._handler_time_sync()

        self._init_params()
        self._stop_autosample()
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Process GET event
        @return next_state, result
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @return (next_state, result)
        @throws InstrumentParameterException
        """
        next_state = None
        result = None
        startup = False

        if len(args) < 1:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')
        params = args[0]
        if len(args) > 1:
            startup = args[1]

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')
        if not isinstance(startup, bool):
            raise InstrumentParameterException('Startup not a bool.')

        self._set_params(params, startup)
        return next_state, result

    def _handler_command_start_direct(self):
        """
        Start direct access
        @return next_state, (next_agent_state, result)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS,
                                             None)

    def _handler_command_start_autosample(self):
        """
        Start autosample
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_resp(InstrumentCommand.LILY_ON,
                          expected_prompt=Prompt.LILY_ON)
        self._do_cmd_resp(InstrumentCommand.NANO_ON,
                          expected_prompt=NANO_STRING)
        self._do_cmd_resp(InstrumentCommand.IRIS_ON,
                          expected_prompt=Prompt.IRIS_ON)
        return ProtocolState.AUTOSAMPLE, (ResourceAgentState.STREAMING, 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_execute_direct(self, data):
        """
        Execute direct access command
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_direct(data)
        self._sent_cmds.append(data)
        return None, (None, None)

    def _handler_direct_access_stop_direct(self):
        """
        Stop direct access
        @return next_state, (next_agent_state, result)
        """
        next_state, next_agent_state = self._handler_unknown_discover()
        if next_state == DriverProtocolState.COMMAND:
            next_agent_state = ResourceAgentState.COMMAND

        return next_state, (next_agent_state, None)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter state handler
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit state handler
        """

    def _handler_acquire_status(self, *args, **kwargs):
        """
        We generate these particles here to avoid the chunker.  This allows us to process status
        messages with embedded messages from the other parts of the instrument.
        @return next_state, (next_agent_state, result)
        """
        ts = ntplib.system_to_ntp_time(time.time())
        parts = []

        for command, particle_class in [
            (InstrumentCommand.SYST_DUMP1, particles.SystStatusParticle),
            (InstrumentCommand.LILY_DUMP1, particles.LilyStatusParticle1),
            (InstrumentCommand.LILY_DUMP2, particles.LilyStatusParticle2),
            (InstrumentCommand.IRIS_DUMP1, particles.IrisStatusParticle1),
            (InstrumentCommand.IRIS_DUMP2, particles.IrisStatusParticle2),
            (InstrumentCommand.NANO_DUMP1, particles.NanoStatusParticle),
        ]:
            result, _ = self._do_cmd_resp(
                command, response_regex=particle_class.regex_compiled())
            parts.append(result)
        sample = self._extract_sample(
            particles.BotptStatusParticle,
            particles.BotptStatusParticle.regex_compiled(),
            NEWLINE.join(parts), ts)

        if self.get_current_state() == ProtocolState.AUTOSAMPLE:
            # acquiring status stops NANO output, restart it
            self._do_cmd_resp(InstrumentCommand.NANO_ON,
                              expected_prompt=NANO_STRING)

        if not sample:
            raise InstrumentProtocolException(
                'Failed to generate status particle')
        return None, (None, sample)

    def _handler_time_sync(self, *args, **kwargs):
        """
        Syncing time starts autosample...
        @return next_state, (next_agent_state, result)
        """
        self._do_cmd_resp(InstrumentCommand.NANO_SET_TIME,
                          expected_prompt=NANO_STRING)
        if self.get_current_state() == ProtocolState.COMMAND:
            self._do_cmd_no_resp(InstrumentCommand.NANO_OFF)
        return None, (None, None)

    def _handler_start_leveling(self):
        """
        Send the start leveling command
        @return next_state, (next_agent_state, result)
        """
        if not self._param_dict.get(Parameter.LILY_LEVELING):
            self._schedule_leveling_timeout()
            self._do_cmd_resp(InstrumentCommand.LILY_START_LEVELING,
                              expected_prompt=Prompt.LILY_START_LEVELING)
            self._param_dict.set_value(Parameter.LILY_LEVELING, True)
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_stop_leveling(self):
        """
        Send the stop leveling command
        @return next_state, (next_agent_state, result)
        """
        if self._param_dict.get(Parameter.LILY_LEVELING):
            self._remove_leveling_timeout()

            self._do_cmd_resp(InstrumentCommand.LILY_STOP_LEVELING,
                              expected_prompt=Prompt.LILY_STOP_LEVELING)
            self._param_dict.set_value(Parameter.LILY_LEVELING, False)

            if self.get_current_state() == ProtocolState.AUTOSAMPLE:
                self._do_cmd_resp(InstrumentCommand.LILY_ON,
                                  expected_prompt=Prompt.LILY_ON)

            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, (None, None)

    def _handler_leveling_timeout(self):
        """
        Leveling has timed out, disable auto-relevel and mark leveling as failed.
        handler_stop_leveling will raise the config change event.
        @throws InstrumentProtocolException
        """
        self._param_dict.set_value(Parameter.AUTO_RELEVEL, False)
        self._param_dict.set_value(Parameter.LEVELING_FAILED, True)
        self._handler_stop_leveling()
        raise InstrumentProtocolException(
            'Leveling failed to complete within timeout, disabling auto-relevel'
        )

    def _handler_start_heater(self, *args, **kwargs):
        """
        Turn the heater on for Parameter.HEAT_DURATION hours
        @return next_state, (next_agent_state, result)
        """
        if not self._param_dict.get(Parameter.HEATER_ON):
            self._do_cmd_resp(InstrumentCommand.HEAT,
                              self._param_dict.get(Parameter.HEAT_DURATION),
                              response_regex=RegexResponse.HEAT)
            self._param_dict.set_value(Parameter.HEATER_ON, True)
            self._schedule_heater_timeout()
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_stop_heater(self, *args, **kwargs):
        """
        Turn the heater on for Parameter.HEAT_DURATION hours
        @return next_state, (next_agent_state, result)
        """
        if self._param_dict.get(Parameter.HEATER_ON):
            self._do_cmd_resp(InstrumentCommand.HEAT,
                              0,
                              response_regex=RegexResponse.HEAT)
            self._param_dict.set_value(Parameter.HEATER_ON, False)
            self._remove_heater_timeout()
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, (None, None)

    def _handler_heater_timeout(self):
        """
        Heater should be finished.  Set HEATER_ON to false.
        """
        self._param_dict.set_value(Parameter.HEATER_ON, False)
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return None, None
Beispiel #40
0
class SatlanticOCR507InstrumentProtocol(CommandResponseInstrumentProtocol):
    """The instrument protocol classes to deal with a Satlantic OCR507 sensor.
    The protocol is a very simple command/response protocol with a few show
    commands and a few set commands.
    Note protocol state machine must be called "self._protocol_fsm"
    """
    _data_particle_type = SatlanticOCR507DataParticle
    _config_particle_type = SatlanticOCR507ConfigurationParticle
    _data_particle_regex = SAMPLE_REGEX
    _config_particle_regex = CONFIG_REGEX

    __metaclass__ = get_logging_metaclass(log_level='debug')

    def __init__(self, callback=None):
        CommandResponseInstrumentProtocol.__init__(self, Prompt, EOLN,
                                                   callback)

        self._last_data_timestamp = None

        self._protocol_fsm = ThreadSafeFSM(SatlanticProtocolState,
                                           SatlanticProtocolEvent,
                                           SatlanticProtocolEvent.ENTER,
                                           SatlanticProtocolEvent.EXIT)

        self._protocol_fsm.add_handler(SatlanticProtocolState.UNKNOWN,
                                       SatlanticProtocolEvent.ENTER,
                                       self._handler_unknown_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.UNKNOWN,
                                       SatlanticProtocolEvent.DISCOVER,
                                       self._handler_unknown_discover)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.ENTER,
                                       self._handler_command_enter)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.GET,
                                       self._handler_command_get)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.SET,
                                       self._handler_command_set)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.START_AUTOSAMPLE,
                                       self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.ACQUIRE_STATUS,
                                       self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(SatlanticProtocolState.COMMAND,
                                       SatlanticProtocolEvent.START_DIRECT,
                                       self._handler_command_start_direct)
        self._protocol_fsm.add_handler(SatlanticProtocolState.AUTOSAMPLE,
                                       SatlanticProtocolEvent.ENTER,
                                       self._handler_autosample_enter)
        self._protocol_fsm.add_handler(
            SatlanticProtocolState.AUTOSAMPLE,
            SatlanticProtocolEvent.STOP_AUTOSAMPLE,
            self._handler_autosample_stop_autosample)
        self._protocol_fsm.add_handler(SatlanticProtocolState.DIRECT_ACCESS,
                                       SatlanticProtocolEvent.ENTER,
                                       self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(
            SatlanticProtocolState.DIRECT_ACCESS,
            SatlanticProtocolEvent.EXECUTE_DIRECT,
            self._handler_direct_access_execute_direct)
        self._protocol_fsm.add_handler(SatlanticProtocolState.DIRECT_ACCESS,
                                       SatlanticProtocolEvent.STOP_DIRECT,
                                       self._handler_direct_access_stop_direct)

        self._protocol_fsm.start(SatlanticProtocolState.UNKNOWN)

        self._add_response_handler(Command.GET, self._parse_get_response)
        self._add_response_handler(Command.SET, self._parse_set_response)
        self._add_response_handler(Command.INVALID,
                                   self._parse_invalid_response)

        self._param_dict.add(
            Parameter.MAX_RATE,
            r"Maximum\ Frame\ Rate:\ (\S+).*?\s*",
            lambda match: match.group(1),
            lambda sVal: '%s' % sVal,
            type=ParameterDictType.STRING,
            display_name="Max Rate",
            value_description=
            "valid values: 0=auto, 0.125, 0.25, 0.5, 1, 2, 4, 8, 10, 12",
            units=Units.HERTZ,
            default_value='0',
            startup_param=True,
            direct_access=True)

        self._param_dict.add(Parameter.INIT_AT,
                             r"Initialize Automatic Telemetry: (off|on)",
                             lambda match: True
                             if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Init AT",
                             default_value=True,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._param_dict.add(Parameter.INIT_SM,
                             r"Initialize Silent Mode: (off|on)",
                             lambda match: True
                             if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Init SM",
                             default_value=True,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._param_dict.add(Parameter.NET_MODE,
                             r"Network Mode: (off|on)",
                             lambda match: True
                             if match.group(1) == 'on' else False,
                             self._boolean_to_off_on,
                             type=ParameterDictType.BOOL,
                             display_name="Net Mode",
                             default_value=False,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             startup_param=True,
                             direct_access=True)

        self._cmd_dict.add(SatlanticCapability.START_AUTOSAMPLE,
                           display_name="Start Autosample")
        self._cmd_dict.add(SatlanticCapability.STOP_AUTOSAMPLE,
                           display_name="Stop Autosample")
        self._cmd_dict.add(SatlanticCapability.ACQUIRE_STATUS,
                           display_name="Acquire Status")

        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

        self._chunker = StringChunker(self.sieve_function)

    @staticmethod
    def _boolean_to_off_on(v):
        """
        Write a boolean value to string formatted for sbe16 set operations.
        @param v a boolean value.
        @retval A yes/no string formatted for sbe16 set operations.
        @throws InstrumentParameterException if value not a bool.
        """

        if not isinstance(v, bool):
            raise InstrumentParameterException('Value %s is not a bool.' %
                                               str(v))
        if v:
            return 'on'
        return 'off'

    @staticmethod
    def sieve_function(raw_data):
        """ The method that splits samples
        """
        log.debug("Raw Data: %r, len: %d", raw_data, len(raw_data))
        log.debug(SAMPLE_REGEX.pattern)
        matchers = [SAMPLE_REGEX, CONFIG_REGEX]
        return_list = []

        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):
        """
        """
        events_out = [x for x in events if SatlanticCapability.has(x)]
        return events_out

    def get_config(self, *args, **kwargs):
        """ Get the entire configuration for the instrument

        @param params The parameters and values to set
        @retval None if nothing was done, otherwise result of FSM event handle
        Should be a dict of parameters and values
        """
        for param in Parameter.list():
            if param != Parameter.ALL:
                self._do_cmd_resp(Command.GET, param, **kwargs)

    def _do_cmd(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers.

        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @retval The fully built command that was sent
        """
        expected_prompt = kwargs.get('expected_prompt', None)
        cmd_line = self._build_default_command(cmd, *args)

        # Send command.
        log.debug('_do_cmd: %s, length=%s' % (repr(cmd_line), len(cmd_line)))
        if len(cmd_line) == 1:
            self._connection.send(cmd_line)
        else:
            for char in cmd_line:
                starttime = time.time()
                self._connection.send(char)
                while len(self._promptbuf
                          ) == 0 or char not in self._promptbuf[-1]:
                    time.sleep(0.0015)
                    if time.time() > starttime + 3:
                        break

            time.sleep(0.115)
            starttime = time.time()
            self._connection.send(EOLN)
            while EOLN not in self._promptbuf[len(cmd_line):len(cmd_line) + 2]:
                time.sleep(0.0015)
                if time.time() > starttime + 3:
                    break

            # Limit resend_check_value from expected_prompt to one of the two below
            resend_check_value = None
            if expected_prompt is not None:
                for check in (Prompt.COMMAND, "SATDI7"):
                    if check in expected_prompt:
                        log.trace('_do_cmd: command: %s, check=%s' %
                                  (cmd_line, check))
                        resend_check_value = check

            # Resend the EOLN if it did not go through the first time
            starttime = time.time()
            if resend_check_value is not None:
                while True:
                    time.sleep(0.1)
                    if time.time() > starttime + 2:
                        log.debug("Sending eoln again.")
                        self._connection.send(EOLN)
                        starttime = time.time()
                    if resend_check_value in self._promptbuf:
                        break
                    if Prompt.INVALID_COMMAND in self._promptbuf:
                        break

        return cmd_line

    def _do_cmd_no_resp(self, cmd, *args, **kwargs):
        """
        Issue a command to the instrument after clearing of buffers. No response is handled as a result of the command.
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        """
        self._do_cmd(cmd, *args, **kwargs)

    def _do_cmd_resp(self, cmd, *args, **kwargs):
        """
        Perform a command-response on the device.
        @param cmd The command to execute.
        @param args positional arguments to pass to the build handler.
        @param expected_prompt kwarg offering a specific prompt to look for
        other than the ones in the protocol class itself.
        @param response_regex kwarg with a compiled regex for the response to
        match. Groups that match will be returned as a string.
        Cannot be supplied with expected_prompt. May be helpful for instruments that do not have a prompt.
        @retval resp_result The (possibly parsed) response result including the
        first instance of the prompt matched. If a regex was used, the prompt
        will be an empty string and the response will be the joined collection of matched groups.
        @raises InstrumentCommandException if the response did not occur in time.
        @raises InstrumentProtocolException if command could not be built or if response was not recognized.
        """
        timeout = kwargs.get('timeout', DEFAULT_CMD_TIMEOUT)
        expected_prompt = kwargs.get(
            'expected_prompt',
            [Prompt.INVALID_COMMAND, Prompt.USAGE, Prompt.COMMAND])
        response_regex = kwargs.get('response_regex', None)

        if response_regex and not isinstance(response_regex, RE_PATTERN):
            raise InstrumentProtocolException(
                'Response regex is not a compiled pattern!')

        if expected_prompt and response_regex:
            raise InstrumentProtocolException(
                'Cannot supply both regex and expected prompt!')

        retry_count = 5
        retry_num = 0
        cmd_line = ""
        result = ""
        prompt = ""
        for retry_num in xrange(retry_count):
            # Clear line and prompt buffers for result.
            self._linebuf = ''
            self._promptbuf = ''

            cmd_line = self._do_cmd(cmd, *args, **kwargs)

            # Wait for the prompt, prepare result and return, timeout exception
            if response_regex:
                result_tuple = self._get_response(
                    timeout,
                    response_regex=response_regex,
                    expected_prompt=expected_prompt)
                result = "".join(result_tuple)
            else:
                (prompt,
                 result) = self._get_response(timeout,
                                              expected_prompt=expected_prompt)

            # Confirm the entire command was sent, otherwise resend retry_count number of times
            if len(cmd_line) > 1 and \
                    (expected_prompt is not None or
                         (response_regex is not None)) \
                    and cmd_line not in result:
                log.debug(
                    '_do_cmd_resp: Send command: %s failed %s attempt, result = %s.',
                    cmd, retry_num, result)
                if retry_num >= retry_count:
                    raise InstrumentCommandException(
                        '_do_cmd_resp: Failed %s attempts sending command: %s'
                        % (retry_count, cmd))
            else:
                break

        log.debug(
            '_do_cmd_resp: Sent command: %s, %s reattempts, expected_prompt=%s, result=%r.',
            cmd_line, retry_num, expected_prompt, result)

        resp_handler = self._response_handlers.get((self.get_current_state(), cmd), None) or \
                       self._response_handlers.get(cmd, None)
        resp_result = None
        if resp_handler:
            resp_result = resp_handler(result, prompt)

        time.sleep(
            0.3)  # give some time for the instrument connection to keep up

        return resp_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)

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state; can be COMMAND or AUTOSAMPLE.
        @retval (next_state, result), (SatlanticProtocolState.COMMAND, ResourceAgentState.IDLE or
        SatlanticProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING) if successful.
        """
        try:
            invalidCommandResponse = self._do_cmd_resp(
                Command.INVALID,
                timeout=3,
                expected_prompt=Prompt.INVALID_COMMAND)
        except InstrumentTimeoutException as ex:
            invalidCommandResponse = None  # The instrument is not in COMMAND: it must be polled or AUTOSAMPLE

        log.debug("_handler_unknown_discover: returned: %s",
                  invalidCommandResponse)
        if invalidCommandResponse:
            return SatlanticProtocolState.COMMAND, ResourceAgentState.IDLE
        # Put the instrument back into full autosample
        self._do_cmd_no_resp(Command.SWITCH_TO_AUTOSAMPLE)
        return SatlanticProtocolState.AUTOSAMPLE, ResourceAgentState.STREAMING

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        # Command device to update parameters and send a config change event.
        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_get(self, *args, **kwargs):
        """Handle getting data from command mode

        @param params List of the parameters to pass to the state
        @retval return (next state, result)
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """Handle setting data from command mode

        @param params Dict of the parameters and values to pass to the state
        @return (next state, result)
        """
        self._set_params(*args, **kwargs)
        return None, None

    def _handler_command_start_autosample(self, params=None, *args, **kwargs):
        """
        Handle getting an start autosample event when in command mode
        @param params List of the parameters to pass to the state
        @return next state (next agent state, result)
        """
        result = None

        self._do_cmd_no_resp(Command.EXIT_AND_RESET)
        time.sleep(RESET_DELAY)
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        next_state = SatlanticProtocolState.AUTOSAMPLE
        next_agent_state = ResourceAgentState.STREAMING

        return next_state, (next_agent_state, result)

    def _handler_command_start_direct(self):
        """
        """
        result = None

        next_state = SatlanticProtocolState.DIRECT_ACCESS
        next_agent_state = ResourceAgentState.DIRECT_ACCESS

        log.debug("_handler_command_start_direct: entering DA mode")
        return next_state, (next_agent_state, result)

    def _handler_command_acquire_status(self, *args, **kwargs):
        """
        Handle SatlanticProtocolState.COMMAND SatlanticProtocolEvent.ACQUIRE_STATUS

        @return next state (next agent state, result)
        """
        next_state = None
        next_agent_state = None
        result = None

        self._do_cmd_no_resp(Command.ID)
        self._do_cmd_no_resp(Command.SHOW_ALL)

        return next_state, (next_agent_state, result)

    ########################################################################
    # Autosample handlers.
    ########################################################################

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Handle SatlanticProtocolState.AUTOSAMPLE SatlanticProtocolEvent.ENTER

        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None

        if not self._confirm_autosample_mode:
            raise InstrumentProtocolException(
                error_code=InstErrorCode.HARDWARE_ERROR,
                msg="Not in the correct mode!")

        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        return next_state, result

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """Handle SatlanticProtocolState.AUTOSAMPLE stop

        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None

        try:
            self._send_break()
            next_state = SatlanticProtocolState.COMMAND
            next_agent_state = ResourceAgentState.COMMAND
        except InstrumentException:
            raise InstrumentProtocolException(
                error_code=InstErrorCode.HARDWARE_ERROR,
                msg="Could not break from autosample!")

        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 _do_cmd_direct(self, cmd):
        """
        Issue an untranslated command to the instrument. No response is handled as a result of the command.
        Overridden: Use _do_cmd to send commands reliably. Remove if digi-serial interface is ever fixed.

        @param cmd The high level command to issue
        """
        self._do_cmd(cmd)

    def _handler_direct_access_execute_direct(self, data):
        """
        """
        next_state = None
        result = None
        next_agent_state = None

        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return next_state, (next_agent_state, result)

    def _handler_direct_access_stop_direct(self):
        """
        """
        next_state, next_agent_state = self._handler_unknown_discover()
        if next_state == DriverProtocolState.COMMAND:
            next_agent_state = ResourceAgentState.COMMAND

        return next_state, (next_agent_state, None)

    ###################################################################
    # Builders
    ###################################################################
    def _build_default_command(self, *args):
        """
        """
        return " ".join(str(x) for x in args)

    ##################################################################
    # Response parsers
    ##################################################################
    def _parse_set_response(self, response, prompt):
        """Determine if a set was successful or not

        @param response What was sent back from the command that was sent
        @param prompt The prompt that was returned from the device
        """
        if prompt == Prompt.COMMAND:
            return True
        return False

    def _parse_get_response(self, response, prompt):
        """ Parse the response from the instrument for a couple of different
        query responses.

        @param response The response string from the instrument
        @param prompt The prompt received from the instrument
        @return The numerical value of the parameter in the known units
        @raise InstrumentProtocolException When a bad response is encountered
        """
        # should end with the response, an eol, and a prompt
        update_dict = self._param_dict.update_many(response)
        if not update_dict or len(update_dict) > 1:
            log.error(
                "Get response set multiple parameters (%r): expected only 1",
                update_dict)
            raise InstrumentProtocolException("Invalid response. Bad command?")

        return self._param_dict.get_all()

    def _parse_invalid_response(self, response, prompt):
        """ Parse the response from the instrument for a couple of different
        query responses.

        @param response The response string from the instrument
        @param prompt The prompt received from the instrument
        @return true iff Prompt.INVALID_COMMAND was returned
        """
        # should end with the response, an eoln, and a prompt
        return Prompt.INVALID_COMMAND == prompt

    ###################################################################
    # Helpers
    ###################################################################
    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        Also called when setting parameters during startup and direct access
        In the event an exception is generated dur
        @throws InstrumentParameterException if parameter does not exist or Maxrate is out of range
        @throws InstrumentCommandException if failed to set
        """

        params = args[0]

        self._verify_not_readonly(*args, **kwargs)
        old_config = self._param_dict.get_config()

        exception = None

        for key in params:
            if key not in self._param_dict._param_dict:
                exception = InstrumentParameterException("Bad parameter: %r" %
                                                         key)
                break
            val = self._param_dict.format(key, params[key])
            log.debug("KEY = %s VALUE = %s", str(key), str(val))
            if key == Parameter.MAX_RATE and float(
                    params[key]) not in VALID_MAXRATES:
                exception = InstrumentParameterException(
                    "Maxrate %s out of range" % val)
                break
            # Check for existance in dict (send only on change)
            if not self._do_cmd_resp(Command.SET, key, val):
                exception = InstrumentCommandException(
                    'Error setting: %s = %s' % (key, val))
                break
            self._param_dict.set_value(key, params[key])

        # Get new param dict config. If it differs from the old config,
        # tell driver superclass to publish a config change event.
        new_config = self._param_dict.get_config()
        log.debug("new_config: %s == old_config: %s", new_config, old_config)
        if old_config != new_config:
            self._do_cmd_resp(Command.SAVE, expected_prompt=Prompt.COMMAND)
            log.debug("configuration has changed.  Send driver event")
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        # Raise any exceptions encountered due to errors setting the parameter(s)
        if exception is not None:
            raise exception

    def _update_params(self, *args, **kwargs):
        """Fetch the parameters from the device, and update the param dict.

        @param args Unused
        @param kwargs Takes timeout value
        """
        old_config = self._param_dict.get_config()
        self.get_config()
        new_config = self._param_dict.get_config()
        if new_config != old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _send_break(self):
        """
        Send break every 0.3 seconds until the Command Console banner is received.
        @throws InstrumentTimeoutException if not Command Console banner not received within 5 seconds.
        """
        self._promptbuf = ""
        self._connection.send(Command.BREAK)
        starttime = time.time()
        resendtime = time.time()
        while True:
            if time.time() > resendtime + 0.3:
                log.debug("Sending break again.")
                self._connection.send(Command.BREAK)
                resendtime = time.time()

            if COMMAND_PATTERN in self._promptbuf:
                break

            if time.time() > starttime + 10:
                raise InstrumentTimeoutException(
                    "Break command failing to stop autosample!")

            time.sleep(0.1)

    def _got_chunk(self, chunk, timestamp):
        """
        extract samples from a chunk of data
        @param chunk: bytes to parse into a sample.
        """
        sample = self._extract_sample(self._data_particle_type, self._data_particle_regex, chunk, timestamp) or \
                 self._extract_sample(self._config_particle_type, self._config_particle_regex, chunk, timestamp)
        if not sample:
            raise InstrumentProtocolException(
                u'unhandled chunk received by _got_chunk: [{0!r:s}]'.format(
                    chunk))
        return sample

    def _confirm_autosample_mode(self):
        """
        Confirm we are in autosample mode.
        This is done by waiting for a sample to come in, and confirming that
        it does or does not.
        @retval True if in autosample mode, False if not
        """
        # timestamp now,
        start_time = self._last_data_timestamp
        # wait a sample period,
        current_maxrate = self._param_dict.get_config()[Parameter.MAX_RATE]
        if current_maxrate is None:
            current_maxrate = 0.125  # During startup, assume the slowest sample rate
        elif current_maxrate <= 0 or current_maxrate > 8:
            current_maxrate = 8  # Effective current maxrate, despite the instrument accepting higher values
        time_between_samples = (1.0 / current_maxrate) + 1
        time.sleep(time_between_samples)
        end_time = self._last_data_timestamp
        log.debug("_confirm_autosample_mode: end_time=%s, start_time=%s" %
                  (end_time, start_time))
        if end_time != start_time:
            log.debug("Confirmed in autosample mode")
            return True
        log.debug("Confirmed NOT in autosample mode")
        return False
Beispiel #41
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """
    __metaclass__ = META_LOGGER

    def __init__(self, prompts, newline, driver_event):
        """
        Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent, ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        handlers = {
            ProtocolState.UNKNOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover),
            ],
            ProtocolState.COMMAND: [
                (ProtocolEvent.ENTER, self._handler_command_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.START_DIRECT, self._handler_command_start_direct),
                (ProtocolEvent.GET, self._handler_command_get),
                (ProtocolEvent.SET, self._handler_command_set),
                (ProtocolEvent.START_TURBO, self._handler_command_start_turbo),
            ],
            ProtocolState.SPINNING_UP: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.AT_SPEED, self._handler_spinning_up_at_speed),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.AT_SPEED: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.ERROR, self._handler_error),
            ],
            ProtocolState.ERROR: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOP_TURBO, self._handler_stop_turbo),
                (ProtocolEvent.CLEAR, self._handler_clear),
                (ProtocolEvent.GET, self._handler_command_get),
            ],
            ProtocolState.SPINNING_DOWN: [
                (ProtocolEvent.ENTER, self._handler_generic_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.ACQUIRE_STATUS, self._handler_acquire_status),
                (ProtocolEvent.STOPPED, self._handler_spinning_down_stopped),
            ],
            ProtocolState.DIRECT_ACCESS: [
                (ProtocolEvent.ENTER, self._handler_direct_access_enter),
                (ProtocolEvent.EXIT, self._handler_generic_exit),
                (ProtocolEvent.STOP_DIRECT, self._handler_direct_access_stop_direct),
                (ProtocolEvent.EXECUTE_DIRECT, self._handler_direct_access_execute_direct),
            ],
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        # 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()

        # Add build and response handlers for device commands.
        for command in InstrumentCommand.list():
            self._add_build_handler(command, self._generic_build_handler)
            self._add_response_handler(command, self._generic_response_handler)

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(Protocol.sieve_function)
        self._max_current_count = 0
        self.initialize_scheduler()

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        @param raw_data: data to be searched
        @return: list of (start,stop) indexes of matches
        """
        return [(m.start(), m.end()) for m in TurboStatusParticle.regex_compiled().finditer(raw_data)]

    def _build_param_dict(self):
        """
        All turbo parameters have the same signature, add them in a loop...
        """
        parameters = {
            Parameter.UPDATE_INTERVAL: {
                'display_name': 'Acquire Status Interval',
                'description': 'Interval between automatic acquire status calls: (5 - 60)',
                'units': Units.SECOND,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_DRIVE_CURRENT: {
                'display_name': 'Maximum Allowable Drive Current',
                'description': 'Maximum allowable drive current at speed: (100 - 200)',
                'units': Prefixes.CENTI + Units.AMPERE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_TEMP_MOTOR: {
                'display_name': 'Maximum Allowable Motor Temperature',
                'description': 'Maximum allowable motor temperature: (5 - 100)',
                'units': Units.DEGREE_CELSIUS,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MAX_TEMP_BEARING: {
                'display_name': 'Maximum Allowable Bearing Temperature',
                'description': 'Maximum allowable bearing temperature: (5 - 100)',
                'units': Units.DEGREE_CELSIUS,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.MIN_SPEED: {
                'display_name': 'Minimum Allowable Turbo Speed',
                'description': 'Minimum allowable turbo speed before RGA is shutdown: (70000 - 90000)',
                'units': Units.REVOLUTION_PER_MINUTE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.TARGET_SPEED: {
                'display_name': 'Target Turbo Speed',
                'description': 'Target turbo speed before RGA is initialized: (70000 - 90000)',
                'units': Units.REVOLUTION_PER_MINUTE,
                'type': ParameterDictType.INT,
                'startup_param': True,
            },
            Parameter.ERROR_REASON: {
                'display_name': 'Turbo Error Reason',
                'description': 'Reason for turbo error state.',
                'visibility': ParameterDictVisibility.READ_ONLY,
                'type': ParameterDictType.STRING,
            }
        }

        reverse_param = Parameter.reverse_dict()
        constraints = ParameterConstraints.dict()

        for name in parameters:
            kwargs = parameters[name]
            if name in constraints:
                _type, minimum, maximum = constraints[name]
                kwargs['val_description'] = '%s value from %d - %d' % (_type, minimum, maximum)
            self._param_dict.add(name, '', None, None, **kwargs)

    def _build_command_dict(self):
        """
        Populate the command dictionary with commands.
        """
        self._cmd_dict.add(Capability.ACQUIRE_STATUS, display_name="Acquire Status")
        self._cmd_dict.add(Capability.START_TURBO, display_name="Start Turbo")
        self._cmd_dict.add(Capability.STOP_TURBO, display_name="Stop Turbo")
        self._cmd_dict.add(Capability.CLEAR, display_name="Clear Error State")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    def _got_chunk(self, chunk, ts):
        """
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and regexes.
        @param chunk: data to be processed
        @param ts: timestamp
        """
        self._extract_sample(TurboStatusParticle, TurboStatusParticle.regex_compiled(), chunk, ts)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        @param events: events to be filtered
        @return: list of events that are in Capability
        """
        return [x for x in events if Capability.has(x)]

    @staticmethod
    def _checksum(s):
        """
        Calculate the turbopump checksum for the given string.
        @param s: string to be checked
        @return: checksum string
        """
        return '%03d' % (sum([ord(x) for x in s]) % 256)

    def _build_turbo_command(self, address, c_type, c, data):
        """
        Build a command for the turbopump
        @param address: target address
        @param c_type: command type (QUERY/SET)
        @param c: command
        @param data: command_data
        @return: command string
        """
        command = '%03d%02d%03d%02d%s' % (address, c_type, c, len(data), data)
        checksum = self._checksum(command)
        return command + checksum

    def _generic_build_handler(self, command, *args, **kwargs):
        """
        Determine if this is a query or set action based on the
        input args.  Dispatch the builder with the appropriate arguments.
        @param command: command to be sent
        @param args: arglist which may contain a value
        @return: command string
        """
        if len(args) == 1:
            # this is a set action
            value = args[0]
            return self._build_turbo_command(ADDRESS, CommandType.SET, command, value) + NEWLINE
        # this is a query
        return self._build_turbo_command(ADDRESS, CommandType.QUERY, command, QUERY) + NEWLINE

    def _generic_response_handler(self, resp, prompt):
        """
        Parse the response from the turbopump.
        @param resp: response
        @param prompt: unused, require to match signature
        @returns: integer value extracted from response
        @throws InstrumentDataException
        """
        my_checksum = self._checksum(resp[:-3])
        if resp[-3:] != my_checksum:
            err_str = 'bad checksum: %r calculated: %r' % (resp, my_checksum)
            raise exceptions.InstrumentDataException(err_str)
        command = int(resp[5:8])
        data_length = int(resp[8:10])
        data = resp[10:-3]
        log.trace('command: %s data: %s', command, data)
        if len(data) != data_length:
            raise exceptions.InstrumentDataException('invalid data length: %r' % resp)
        if command not in InstrumentCommand.list():
            raise exceptions.InstrumentDataException('command not found: %r' % resp)
        return int(data)

    def _wakeup(self, timeout, delay=1):
        """
        Not valid for this instrument
        """

    def _build_scheduler(self):
        """
        Build a scheduler for periodic status updates
        """
        job_name = ScheduledJob.ACQUIRE_STATUS
        config = {
            DriverConfigKey.SCHEDULER: {
                job_name: {
                    DriverSchedulerConfigKey.TRIGGER: {
                        DriverSchedulerConfigKey.TRIGGER_TYPE: TriggerType.INTERVAL,
                        DriverSchedulerConfigKey.SECONDS: self._param_dict.get(Parameter.UPDATE_INTERVAL)
                    },
                }
            }
        }

        self.set_init_params(config)
        self._add_scheduler_event(ScheduledJob.ACQUIRE_STATUS, ProtocolEvent.ACQUIRE_STATUS)

    def _update_params(self, *args, **kwargs):
        """
        Parameters are NOT set in the instrument by this method, as all parameters are driver only.
        """

    def _set_params(self, *args, **kwargs):
        """
        Set parameters, raise a CONFIG_CHANGE event if necessary.
        @throws InstrumentParameterException
        """
        self._verify_not_readonly(*args, **kwargs)
        params_to_set = args[0]
        old_config = self._param_dict.get_all()

        # check if in range
        constraints = ParameterConstraints.dict()
        parameters = Parameter.reverse_dict()

        # step through the list of parameters
        for key, val in params_to_set.iteritems():
            # if constraint exists, verify we have not violated it
            constraint_key = parameters.get(key)
            if constraint_key in constraints:
                var_type, minimum, maximum = constraints[constraint_key]
                try:
                    value = var_type(val)
                except ValueError:
                    raise exceptions.InstrumentParameterException(
                        'Unable to verify type - parameter: %s value: %s' % (key, val))
                if val < minimum or val > maximum:
                    raise exceptions.InstrumentParameterException(
                        'Value out of range - parameter: %s value: %s min: %s max: %s' %
                        (key, val, minimum, maximum))

        # all constraints met or no constraints exist, set the values
        for key, val in params_to_set.iteritems():
            if key in old_config:
                self._param_dict.set_value(key, val)
            else:
                raise exceptions.InstrumentParameterException(
                    'Attempted to set unknown parameter: %s value: %s' % (key, val))
        new_config = self._param_dict.get_all()

        # If we changed anything, raise a CONFIG_CHANGE event
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

    def _send_command_with_retry(self, command, value=None, sleep_time=1, max_retries=MAX_RETRIES):
        """
        Attempt to send a command up to max_retries times.  Protocol state will move to ERROR if we fail to
        receive a response after max_retries attempts.
        @throws InstrumentTimeoutException
        """
        for attempt in xrange(1, max_retries + 1):
            try:
                if value is None:
                    result = self._do_cmd_resp(command, response_regex=TURBO_RESPONSE, timeout=TIMEOUT)
                else:
                    result = self._do_cmd_resp(command, value, response_regex=TURBO_RESPONSE, timeout=TIMEOUT)
                return result
            except exceptions.InstrumentTimeoutException:
                log.error('Error sending command: %s, attempt %d', command, attempt)
                time.sleep(sleep_time)

        # set the error reason
        self._param_dict.set_value(Parameter.ERROR_REASON, 'Unable to command the turbo')
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        self._async_raise_fsm_event(ProtocolEvent.ERROR)
        raise exceptions.InstrumentTimeoutException('Failed to command the turbo: %s' % command)

    ########################################################################
    # Generic handlers.
    ########################################################################

    def _handler_generic_enter(self, *args, **kwargs):
        """
        Generic enter handler when no specific action is needed.
        """
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_generic_exit(self, *args, **kwargs):
        """
        Generic exit handler when no specific action is needed.
        """

    def _handler_acquire_status(self, *args, **kwargs):
        """
        Query the instrument for the following status items:
            drive current
            drive voltage
            bearing temp
            motor temp
            rotation speed

        Verify no values exceed the limits specified in the parameter dictionary.
        @returns: next_state, (next_agent_state, result)
        @throws InstrumentStateException
        """
        responses = {}

        # query the turbo for the speed/temp/current values
        for command in [InstrumentCommand.DRIVE_CURRENT, InstrumentCommand.DRIVE_VOLTAGE,
                        InstrumentCommand.TEMP_BEARING, InstrumentCommand.TEMP_MOTOR,
                        InstrumentCommand.ROTATION_SPEED_ACTUAL]:
            responses[command] = self._send_command_with_retry(command)

        # check the current driver state
        current_state = self.get_current_state()
        error = None

        # Check for over temperature conditions
        if responses[InstrumentCommand.TEMP_MOTOR] > self._param_dict.get(Parameter.MAX_TEMP_MOTOR) or \
                responses[InstrumentCommand.TEMP_BEARING] > self._param_dict.get(Parameter.MAX_TEMP_BEARING):
            error = 'Over temp error - Motor: %d Bearing: %d' % (responses[InstrumentCommand.TEMP_MOTOR],
                                                                 responses[InstrumentCommand.TEMP_BEARING])

        # Check if we were up to speed but have dipped below MIN_SPEED
        elif current_state == ProtocolState.AT_SPEED:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] < self._param_dict.get(Parameter.MIN_SPEED):
                error = 'Fell below min speed: %d' % responses[InstrumentCommand.ROTATION_SPEED_ACTUAL]

            # or if we're up to speed and we have exceeded MAX_DRIVE_CURRENT more than 3 subsequent intervals
            if responses[InstrumentCommand.DRIVE_CURRENT] > self._param_dict.get(Parameter.MAX_DRIVE_CURRENT):
                self._max_current_count += 1
                if self._max_current_count > CURRENT_STABILIZE_RETRIES:
                    error = 'Turbo current draw to high: %d' % responses[InstrumentCommand.DRIVE_CURRENT]
            else:
                self._max_current_count = 0

        if error:
            self._param_dict.set_value(Parameter.ERROR_REASON, error)
            self._async_raise_fsm_event(ProtocolEvent.ERROR)
            self._driver_event(DriverAsyncEvent.ERROR, error)

        # now check if up to speed when spinning up
        elif current_state == ProtocolState.SPINNING_UP:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] >= self._param_dict.get(Parameter.TARGET_SPEED):
                self._async_raise_fsm_event(ProtocolEvent.AT_SPEED)

        # or maybe we've stopped while spinning down (we'll consider < MIN_SPEED as stopped...)
        elif current_state == ProtocolState.SPINNING_DOWN:
            if responses[InstrumentCommand.ROTATION_SPEED_ACTUAL] <= self._param_dict.get(Parameter.MIN_SPEED):
                self._async_raise_fsm_event(ProtocolEvent.STOPPED)

        return None, (None, responses)

    def _handler_stop_turbo(self):
        """
        Stop the turbo
        @returns: next_state, (next_agent_state, result)
        """
        for command in [InstrumentCommand.PUMP_STATION, InstrumentCommand.MOTOR_PUMP]:
            self._send_command_with_retry(command, value=FALSE)

        return ProtocolState.SPINNING_DOWN, (ResourceAgentState.BUSY, None)

    ########################################################################
    # Unknown handlers.
    ########################################################################

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state.  This instrument always discovers to COMMAND
        @returns: next_state, next_agent_state
        """
        return ProtocolState.COMMAND, ResourceAgentState.IDLE

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        """
        self._init_params()

        # delete the scheduled acquire status job, if it exists.
        # This portion of the MASSP is powered OFF the majority of the time
        # so acquire_status should not be running
        try:
            self._remove_scheduler(ScheduledJob.ACQUIRE_STATUS)
        except KeyError:
            pass

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameter
        @returns: next_state, result
        """
        return self._handler_get(*args, **kwargs)

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        @returns: next_state, result
        """
        next_state = None
        result = None
        self._set_params(*args, **kwargs)

        return next_state, result

    def _handler_command_start_direct(self):
        """
        Start direct access
        @returns: next_state, (next_agent_state, result)
        """
        return ProtocolState.DIRECT_ACCESS, (ResourceAgentState.DIRECT_ACCESS, None)

    def _handler_command_start_turbo(self):
        """
        Start the turbo, periodic status scheduler
        @returns: next_state, (next_agent_state, result)
        """
        for command in [InstrumentCommand.PUMP_STATION, InstrumentCommand.MOTOR_PUMP]:
            self._send_command_with_retry(command, value=TRUE)
        # start the acquire_status scheduler
        self._build_scheduler()
        return ProtocolState.SPINNING_UP, (ResourceAgentState.BUSY, 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_execute_direct(self, data):
        """
        Forward a direct access command to the instrument
        @returns: next_state, (next_agent_state, result)
        """
        self._do_cmd_direct(data)

        # add sent command to list for 'echo' filtering in callback
        self._sent_cmds.append(data)

        return None, (None, None)

    def _handler_direct_access_stop_direct(self):
        """
        Stop direct access, return to COMMAND
        @returns: next_state, (next_agent_state, result)
        """
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)

    ########################################################################
    # Spinning up/down handlers.
    ########################################################################

    def _handler_spinning_up_at_speed(self):
        """
        Instrument has reached operating speed, transition states.
        @returns: next_state, next_agent_state
        """
        return ProtocolState.AT_SPEED, ResourceAgentState.BUSY

    def _handler_spinning_down_stopped(self):
        """
        Instrument has spun down, transition states.
        @returns: next_state, next_agent_state
        """
        self._async_agent_state_change(ResourceAgentState.COMMAND)
        return ProtocolState.COMMAND, ResourceAgentState.COMMAND

    ########################################################################
    # Error handlers.
    ########################################################################

    def _handler_error(self, *args, **kwargs):
        """
        Error detected, go to the ERROR state.
        @returns: next_state, (next_agent_state, result)
        """
        return ProtocolState.ERROR, (ResourceAgentState.COMMAND, None)

    def _handler_clear(self, *args, **kwargs):
        """
        User requests error state be cleared, go to COMMAND.
        @returns: next_state, (next_agent_state, result)
        """
        self._param_dict.set_value(Parameter.ERROR_REASON, '')
        self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        return ProtocolState.COMMAND, (ResourceAgentState.COMMAND, None)
Beispiel #42
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class
    Subclasses CommandResponseInstrumentProtocol
    """

    __metaclass__ = get_logging_metaclass(log_level='trace')

    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.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.START_AUTOSAMPLE,
                                       self._handler_command_autosample)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.ACQUIRE_STATUS,
                                       self._handler_command_acquire_status)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.GET,
                                       self._handler_command_get)
        self._protocol_fsm.add_handler(ProtocolState.COMMAND,
                                       ProtocolEvent.SET,
                                       self._handler_command_set)

        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.STOP_AUTOSAMPLE,
                                       self._handler_autosample_stop)
        self._protocol_fsm.add_handler(ProtocolState.AUTOSAMPLE,
                                       ProtocolEvent.GET,
                                       self._handler_command_get)

        # Construct the parameter dictionary containing device parameters,
        # current parameter values, and set formatting functions.
        self._build_driver_dict()
        self._build_command_dict()
        self._build_param_dict()

        # Add sample handlers.

        # State state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # commands sent sent to device to be filtered in responses for telnet DA
        self._sent_cmds = []

        self._chunker = StringChunker(self.sieve_function)

        log.info('processing particles with %d workers', POOL_SIZE)
        self._process_particles = True
        self._pending_particles = deque()
        self._processing_pool = multiprocessing.Pool(POOL_SIZE)

        self._particles_thread = Thread(target=self.particles_thread)
        self._particles_thread.setDaemon(True)
        self._particles_thread.start()

    def particles_thread(self):
        log.info('Starting particles generation thread.')
        processing_pool = self._processing_pool
        try:
            futures = {}

            while self._process_particles or futures:
                # Pull all processing requests from our request deque
                # Unless we have been instructed to terminate
                while True and self._process_particles:
                    try:
                        filepath, timestamp = self._pending_particles.popleft()
                        log.info('Received RAW file to process: %r %r',
                                 filepath, timestamp)
                        # Schedule for processing
                        # parse_datagram_file takes the filepath and returns a
                        # tuple containing the metadata and timestamp for creation
                        # of the particle
                        futures[(filepath,
                                 timestamp)] = processing_pool.apply_async(
                                     parse_particles_file, (filepath, ))
                    except IndexError:
                        break

                # Grab our keys here, to avoid mutating the dictionary while iterating
                future_keys = sorted(futures)
                if future_keys:
                    log.debug('Awaiting completion of %d particles',
                              len(future_keys))

                for key in future_keys:
                    future = futures[key]
                    if future.ready():
                        try:
                            # Job complete, remove the future from our dictionary and generate a particle
                            result = future.get()
                        except Exception as e:
                            result = e

                        futures.pop(key, None)

                        if isinstance(result, Exception):
                            self._driver_event(DriverAsyncEvent.ERROR, result)
                            continue

                        if result is not None:
                            metadata, internal_timestamp, data_times, power_data_dict, frequencies = result

                            filepath, timestamp = key
                            log.info(
                                'Completed particles with filepath: %r timestamp: %r',
                                filepath, timestamp)

                            metadata_particle = ZplscBInstrumentDataParticle(
                                metadata,
                                port_timestamp=timestamp,
                                internal_timestamp=internal_timestamp,
                                preferred_timestamp=DataParticleKey.
                                INTERNAL_TIMESTAMP)
                            parsed_sample = metadata_particle.generate()

                            if self._driver_event:
                                self._driver_event(DriverAsyncEvent.SAMPLE,
                                                   parsed_sample)

                            for counter, data_timestamp in enumerate(
                                    data_times):
                                zp_data = {
                                    ZplscBParticleKey.FREQ_CHAN_1:
                                    frequencies[1],
                                    ZplscBParticleKey.VALS_CHAN_1:
                                    list(power_data_dict[1][counter]),
                                    ZplscBParticleKey.FREQ_CHAN_2:
                                    frequencies[2],
                                    ZplscBParticleKey.VALS_CHAN_2:
                                    list(power_data_dict[2][counter]),
                                    ZplscBParticleKey.FREQ_CHAN_3:
                                    frequencies[3],
                                    ZplscBParticleKey.VALS_CHAN_3:
                                    list(power_data_dict[3][counter]),
                                }

                                sample_particle = ZplscBSampleDataParticle(
                                    zp_data,
                                    port_timestamp=timestamp,
                                    internal_timestamp=data_timestamp,
                                    preferred_timestamp=DataParticleKey.
                                    INTERNAL_TIMESTAMP)

                                parsed_sample_particles = sample_particle.generate(
                                )

                                if self._driver_event:
                                    self._driver_event(
                                        DriverAsyncEvent.SAMPLE,
                                        parsed_sample_particles)

                time.sleep(1)

        finally:
            if processing_pool:
                processing_pool.terminate()
                processing_pool.join()

    def shutdown(self):
        log.info('Shutting down ZPLSC protocol')
        super(Protocol, self).shutdown()
        # Do not add any more datagrams to the processing queue
        self._process_particles = False
        # Await completed processing of all datagrams for a maximum of 10 minutes
        log.info('Joining particles_thread')
        self._particles_thread.join(timeout=600)
        log.info('Completed ZPLSC protocol shutdown')

    def _build_param_dict(self):
        """
        Populate the parameter dictionary with parameters.
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        self._param_dict.add(
            Parameter.SCHEDULE,
            r'schedule:\s+(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="Schedule",
            description=
            "Large block of text used to create the .yaml file defining the sampling schedule.",
            startup_param=True,
            default_value=yaml.dump(DEFAULT_CONFIG, default_flow_style=False))

        self._param_dict.add(
            Parameter.FTP_IP_ADDRESS,
            r'ftp address:\s+(\d\d\d\d\.\d\d\d\d\.\d\d\d\d\.\d\d\d)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP IP Address",
            description=
            "IP address the driver uses to connect to the instrument FTP server.",
            startup_param=True,
            default_value=DEFAULT_HOST)

        self._param_dict.add(
            Parameter.FTP_USERNAME,
            r'username:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP User Name",
            description="Username used to connect to the FTP server.",
            startup_param=True,
            default_value=USER_NAME)

        self._param_dict.add(
            Parameter.FTP_PASSWORD,
            r'password:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Password",
            description="Password used to connect to the FTP server.",
            startup_param=True,
            default_value=PASSWORD)

        self._param_dict.add(
            Parameter.FTP_PORT,
            r'port:(.*)',
            lambda match: match.group(1),
            str,
            type=ParameterDictType.STRING,
            display_name="FTP Port",
            description=
            "Location on the OOI infrastructure where .raw files stored.",
            startup_param=True,
            default_value=DEFAULT_PORT)

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, True)

    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.ACQUIRE_STATUS,
                           display_name="Acquire Status")
        self._cmd_dict.add(Capability.DISCOVER, display_name='Discover')

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """
        return [x for x in events if Capability.has(x)]

    ########################################################################
    # 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)

    @staticmethod
    def _handler_unknown_exit(*args, **kwargs):
        """
        Exit unknown state.
        """
        pass

    def _handler_unknown_discover(self, *args, **kwargs):
        """
        Discover current state
        @retval next_state, (next_state, result)
        """
        next_state = ProtocolState.COMMAND
        result = []

        # Try to get the status to check if the instrument is alive
        host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
        port = self._param_dict.get_config_value(Parameter.FTP_PORT)
        response = self._url_request(host, port, '/status.json')

        if response is None:
            error_msg = "_handler_unknown_discover: Unable to connect to host: %s" % host
            log.error(error_msg)
            raise InstrumentConnectionException(error_msg)

        return next_state, (next_state, result)

    ########################################################################
    # Command handlers.
    ########################################################################
    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws InstrumentTimeoutException if the device cannot be woken.
        @throws InstrumentProtocolException if the update commands and not recognized.
        """
        self._init_params()

        # Tell driver superclass to send a state change event.
        # Superclass will query the state.
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    @staticmethod
    def _handler_command_exit(*args, **kwargs):
        """
        Exit command state.
        """
        pass

    def _handler_command_get(self, *args, **kwargs):
        """
        Get parameters while in the command state.
        @param params List of the parameters to pass to the state
        @retval returns (next_state, result) where result is a dict {}. No
            agent state changes happening with Get, so no next_agent_state
        @throw InstrumentParameterException for invalid parameter
        """
        result_vals = {}

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]

        except IndexError:
            raise InstrumentParameterException(
                '_handler_command_get requires a parameter dict.')

        if Parameter.ALL in params:
            log.debug("Parameter ALL in params")
            params = Parameter.list()
            params.remove(Parameter.ALL)

        log.debug("_handler_command_get: params = %s", params)

        if params is None or not isinstance(params, list):
            raise InstrumentParameterException(
                "GET parameter list not a list!")

        # fill the return values from the update
        for param in params:
            if not Parameter.has(param):
                raise InstrumentParameterException("Invalid parameter!")
            result_vals[param] = self._param_dict.get(param)
            self._param_dict.get_config_value(param)
        result = result_vals

        log.debug("Get finished, next_state: %s, result: %s", None, result)
        return None, result

    def _handler_command_set(self, *args, **kwargs):
        """
        Set parameter
        @retval next state, result
        """
        startup = False

        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                '_handler_command_set: command requires a parameter dict.')

        try:
            startup = args[1]
        except IndexError:
            pass

        if not isinstance(params, dict):
            raise InstrumentParameterException('Set parameters not a dict.')

        # For each key, val in the params, set the param dictionary.
        old_config = self._param_dict.get_config()
        self._set_params(params, startup)

        new_config = self._param_dict.get_config()
        if old_config != new_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)

        return None, None

    def _set_params(self, *args, **kwargs):
        """
        Issue commands to the instrument to set various parameters
        """
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        # verify param is not readonly param
        self._verify_not_readonly(*args, **kwargs)

        for key, val in params.iteritems():
            log.debug("KEY = %s VALUE = %s", key, val)
            self._param_dict.set_value(key, val)
            if key == Parameter.SCHEDULE:
                self._ftp_schedule_file()

                # Load the schedule file
                host = self._param_dict.get(Parameter.FTP_IP_ADDRESS)
                port = self._param_dict.get_config_value(Parameter.FTP_PORT)
                log.debug("_set_params: stop the current schedule file")
                self._url_request(host, port, '/stop_schedule', data={})
                log.debug("_set_params: upload driver YAML file to host %s",
                          host)
                res = self._url_request(host,
                                        port,
                                        '/load_schedule',
                                        data=json.dumps(
                                            {'filename': YAML_FILE_NAME}))
                log.debug("_set_params: result from load = %s", res)

        log.debug("set complete, update params")

    def _ftp_schedule_file(self):
        """
        Construct a YAML schedule file and
        ftp the file to the Instrument server
        """
        # Create a temporary file and write the schedule YAML information to the file
        try:
            config_file = tempfile.TemporaryFile()
            log.debug("temporary file created")

            if config_file is None or not isinstance(config_file, file):
                raise InstrumentException("config_file is not a temp file!")

            config_file.write(self._param_dict.get(Parameter.SCHEDULE))
            config_file.seek(0)
            log.debug("finished writing config file:\n%r",
                      self._param_dict.get(Parameter.SCHEDULE))

        except Exception as e:
            log.error("Create schedule YAML file exception: %s", e)
            raise e

        #  FTP the schedule file to the ZPLSC server
        host = ''

        try:
            log.debug("Create a ftp session")
            host = self._param_dict.get_config_value(Parameter.FTP_IP_ADDRESS)
            log.debug("Got host ip address %s", host)

            ftp_session = ftplib.FTP()
            ftp_session.connect(host)
            ftp_session.login(USER_NAME, PASSWORD)
            log.debug("ftp session was created...")

            ftp_session.set_pasv(False)
            ftp_session.cwd("config")

            ftp_session.storlines('STOR ' + YAML_FILE_NAME, config_file)
            files = ftp_session.dir()

            log.debug("*** Config yaml file sent: %s", files)

            ftp_session.quit()
            config_file.close()

        except (ftplib.socket.error, ftplib.socket.gaierror), e:
            log.error("ERROR: cannot reach FTP Host %s: %s ", host, e)
            raise InstrumentException("ERROR: cannot reach FTP Host %s " %
                                      host)

        log.debug("*** FTP %s to ftp host %s successfully", YAML_FILE_NAME,
                  host)
Beispiel #43
0
class Protocol(Pco2wProtocol):
    """
    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.
        """

        # Build protocol state machine.
        self._protocol_fsm = ThreadSafeFSM(
            ProtocolState, ProtocolEvent,
            ProtocolEvent.ENTER, ProtocolEvent.EXIT)

        # Construct protocol superclass.
        Pco2wProtocol.__init__(self, prompts, newline, driver_event)

        # Build protocol state machine.

        self._protocol_fsm.add_handler(
            ProtocolState.COMMAND, ProtocolEvent.RUN_EXTERNAL_PUMP,
            self._handler_command_run_external_pump)

        # this state would be entered whenever a RUN_EXTERNAL_PUMP event
        # occurred while in the COMMAND state
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.ENTER,
            self._execution_state_enter)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.EXIT,
            self._execution_state_exit)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.EXECUTE,
            self._handler_run_external_pump_execute)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.SUCCESS,
            self._execution_success_to_command_state)
        self._protocol_fsm.add_handler(
            ProtocolState.RUN_EXTERNAL_PUMP, ProtocolEvent.TIMEOUT,
            self._execution_timeout_to_command_state)

        # Add build handlers for device commands.
        ### primarily defined in base class
        self._add_build_handler(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1, self._build_simple_command)
        # Add response handlers for device commands.
        ### primarily defined in base class
        self._add_response_handler(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1, self._parse_response_sample_dev1)

        # Add sample handlers

        # Start state machine in UNKNOWN state.
        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        # build the chunker
        self._chunker = StringChunker(Protocol.sieve_function)

        self._engineering_parameters.append(Parameter.EXTERNAL_PUMP_DELAY)

    def _filter_capabilities(self, events):
        """
        Return a list of currently available capabilities.
        """

        return [x for x in events if Capability.has(x)]

    ########################################################################
    # Command handlers.
    ########################################################################

    def _handler_command_run_external_pump(self):
        """
        Run external pump
        """
        next_state = ProtocolState.RUN_EXTERNAL_PUMP
        result = []

        return next_state, (next_state, result)

    ########################################################################
    # Run external pump handlers.
    ########################################################################

    def _handler_run_external_pump_execute(self, *args, **kwargs):
        """
        Execute run external pump (dev1) command
        """
        next_state = None
        result = []

        try:
            self._take_dev1_sample()

            log.debug('Protocol._handler_run_external_pump_execute(): SUCCESS')

            self._async_raise_fsm_event(ProtocolEvent.SUCCESS)

        except InstrumentTimeoutException:

            log.error('Protocol._handler_run_external_pump_execute(): TIMEOUT')

            self._async_raise_fsm_event(ProtocolEvent.TIMEOUT)

        return next_state, (next_state, result)

    ########################################################################
    # Response handlers.
    ########################################################################

    def _parse_response_sample_dev1(self, response, prompt):
        """
        Parse response to take dev1 sample from instrument
        """

    def _take_dev1_sample(self):
        """
        Run external pump and wait for dev1 sample
        """
        log.debug('Protocol._take_dev1_sample(): Take Dev1 Sample START')

        start_time = time.time()

        dev1_timeout = self._param_dict.get(Parameter.EXTERNAL_PUMP_SETTINGS)

        log.debug('Protocol._take_dev1_sample(): Dev1 Timeout = %s', dev1_timeout)

        ## An exception is raised if timeout is hit.
        self._do_cmd_resp(InstrumentCommand.PCO2WB_ACQUIRE_SAMPLE_DEV1,
                          timeout=dev1_timeout,
                          response_regex=PCO2WB_DEV1_SAMPLE_REGEX_MATCHER)

        sample_time = time.time() - start_time

        log.debug('Protocol._take_dev1_sample(): Dev1 Sample took %s to FINISH', sample_time)

    def _pre_sample_processing(self):
        """
        Run external pump and wait for equilibrium
        """

        self._take_dev1_sample()

        external_pump_delay = self._param_dict.get(Parameter.EXTERNAL_PUMP_DELAY)

        log.debug('Protocol._pre_sample_processing(): Delaying for %d seconds', external_pump_delay)

        time.sleep(external_pump_delay)

    @staticmethod
    def sieve_function(raw_data):
        """
        The method that splits samples
        :param raw_data: data to filter
        """

        return_list = []

        sieve_matchers = [SAMI_REGULAR_STATUS_REGEX_MATCHER,
                          PCO2W_SAMPLE_REGEX_MATCHER,
                          PCO2WB_DEV1_SAMPLE_REGEX_MATCHER,
                          PCO2WB_CONFIGURATION_REGEX_MATCHER,
                          SAMI_ERROR_REGEX_MATCHER]

        for matcher in sieve_matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _got_chunk(self, chunk, timestamp):
        """
        The base class got_data has gotten a chunk from the chunker. Pass it to
        extract_sample with the appropriate particle objects and REGEXes.
        """

        if any([
                self._extract_sample(SamiRegularStatusDataParticle, SAMI_REGULAR_STATUS_REGEX_MATCHER,
                                     chunk, timestamp),
                self._extract_sample(Pco2wConfigurationDataParticle, PCO2WB_CONFIGURATION_REGEX_MATCHER,
                                     chunk, timestamp)]):
            return

        dev1_sample = self._extract_sample(Pco2wbDev1SampleDataParticle, PCO2WB_DEV1_SAMPLE_REGEX_MATCHER, chunk,
                                           timestamp)
        sami_sample = self._extract_sample(Pco2wSamiSampleDataParticle, PCO2W_SAMPLE_REGEX_MATCHER_NORMAL, chunk,
                                           timestamp)
        if sami_sample is None:
            sami_sample = self._extract_sample(Pco2wSamiSampleCalibrationDataParticle, PCO2W_SAMPLE_REGEX_MATCHER_CAL,
                                               chunk, timestamp)

        log.debug('Protocol._got_chunk(): get_current_state() == %s', self.get_current_state())

        if sami_sample:
            self._verify_checksum(chunk, PCO2W_SAMPLE_REGEX_MATCHER)
        elif dev1_sample:
            self._verify_checksum(chunk, PCO2WB_DEV1_SAMPLE_REGEX_MATCHER)

    ########################################################################
    # Build Command, Driver and Parameter dictionaries
    ########################################################################

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """

        Pco2wProtocol._build_command_dict(self)

        self._cmd_dict.add(Capability.RUN_EXTERNAL_PUMP, display_name="Run External Pump")

    def _build_param_dict(self):
        """
        For each parameter key, add match string, match lambda function,
        and value formatting function for set commands.
        """

        Pco2wProtocol._build_param_dict(self)

        ### example configuration string
        # VALID_CONFIG_STRING = 'CEE90B0002C7EA0001E133800A000E100402000E10010B' + \
        #                       '000000000D000000000D000000000D07' + \
        #                       '1020FF54181C01003814' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '000000000000000000000000000000000000000000000000000' + \
        #                       '0000000000000000000000000000' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + \
        #                       'FFFFFFFFFFFFFFFFFFFFFFFFFFFFF' + NEWLINE
        #
        ###

        configuration_string_regex = self._get_configuration_string_regex()

        self._param_dict.add(Parameter.MODE_BITS, configuration_string_regex,
                             lambda match: int(match.group(4), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x02,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             display_name='Mode Bits',
                             description='Switch bits for sample scheduling.')

        self._param_dict.add(Parameter.DEVICE1_SAMPLE_INTERVAL, configuration_string_regex,
                             lambda match: int(match.group(8), 16),
                             lambda x: self._int_to_hexstring(x, 6),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x000E10,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             display_name='Device 1 Sample Interval',
                             description='',
                             units=Units.SECOND)

        self._param_dict.add(Parameter.DEVICE1_DRIVER_VERSION, configuration_string_regex,
                             lambda match: int(match.group(9), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x01,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             display_name='Device 1 Driver Version',
                             description='')

        self._param_dict.add(Parameter.DEVICE1_PARAMS_POINTER, configuration_string_regex,
                             lambda match: int(match.group(10), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x0B,
                             visibility=ParameterDictVisibility.IMMUTABLE,
                             display_name='Device 1 Parameter Pointer',
                             description='Pointer to device 1 parameters (offset from position 76).')

        self._param_dict.add(Parameter.EXTERNAL_PUMP_SETTINGS, configuration_string_regex,
                             lambda match: int(match.group(30), 16),
                             lambda x: self._int_to_hexstring(x, 2),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=True,
                             default_value=0x1E,
                             range=(0, 0xFF),
                             visibility=ParameterDictVisibility.READ_WRITE,
                             display_name='External Pump Settings',
                             description='Timeout for taking a device 1 sample.',
                             units=Units.SECOND)

        ## Engineering parameter to set delay after running external pump to take a sample, set as startup parameter
        ##   because it is configurable by the user and should be reapplied on application of startup parameters.
        self._param_dict.add(Parameter.EXTERNAL_PUMP_DELAY, r'External pump delay = ([0-9]+)',
                             lambda match: match.group(1),
                             lambda x: int(x),
                             type=ParameterDictType.INT,
                             startup_param=True,
                             direct_access=False,
                             default_value=360,
                             range=(0, 86400),  # up to 1 day
                             visibility=ParameterDictVisibility.READ_WRITE,
                             display_name='External Pump Delay',
                             description='Time to wait before taking a sample after running the external pump.',
                             units=Units.SECOND)

    ########################################################################
    # Overridden base class methods
    ########################################################################

    def _get_specific_configuration_string_parameters(self):
        """
        Overridden by device specific subclasses.
        """

        # An ordered list of parameters, can not use unordered dict
        # PCO2W driver extends the base class (SamiParameter)
        parameter_list = [Parameter.START_TIME_FROM_LAUNCH,
                          Parameter.STOP_TIME_FROM_START,
                          Parameter.MODE_BITS,
                          Parameter.SAMI_SAMPLE_INTERVAL,
                          Parameter.SAMI_DRIVER_VERSION,
                          Parameter.SAMI_PARAMS_POINTER,
                          Parameter.DEVICE1_SAMPLE_INTERVAL,
                          Parameter.DEVICE1_DRIVER_VERSION,
                          Parameter.DEVICE1_PARAMS_POINTER,
                          Parameter.DEVICE2_SAMPLE_INTERVAL,
                          Parameter.DEVICE2_DRIVER_VERSION,
                          Parameter.DEVICE2_PARAMS_POINTER,
                          Parameter.DEVICE3_SAMPLE_INTERVAL,
                          Parameter.DEVICE3_DRIVER_VERSION,
                          Parameter.DEVICE3_PARAMS_POINTER,
                          Parameter.PRESTART_SAMPLE_INTERVAL,
                          Parameter.PRESTART_DRIVER_VERSION,
                          Parameter.PRESTART_PARAMS_POINTER,
                          Parameter.GLOBAL_CONFIGURATION,
                          Parameter.PUMP_PULSE,
                          Parameter.PUMP_DURATION,
                          Parameter.SAMPLES_PER_MEASUREMENT,
                          Parameter.CYCLES_BETWEEN_BLANKS,
                          Parameter.NUMBER_REAGENT_CYCLES,
                          Parameter.NUMBER_BLANK_CYCLES,
                          Parameter.FLUSH_PUMP_INTERVAL,
                          Parameter.PUMP_SETTINGS,
                          Parameter.NUMBER_EXTRA_PUMP_CYCLES,
                          Parameter.EXTERNAL_PUMP_SETTINGS]

        return parameter_list

    def _get_configuration_string_regex(self):
        """
        Get configuration string regex.
        @retval configuration string regex.
        """
        return PCO2WB_CONFIGURATION_REGEX

    def _get_configuration_string_regex_matcher(self):
        """
        Get config string regex matcher.
        @retval configuration string regex matcher
        """
        return PCO2WB_CONFIGURATION_REGEX_MATCHER
Beispiel #44
0
class Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class for SBE16 DOSTA driver.
    """

    particles = [
        DoSampleParticle,
    ]

    def __init__(self, prompts, newline, driver_event):
        """
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The SBE16 newline.
        @param driver_event Driver process event callback.
        """
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline,
                                                   driver_event)

        # This driver does not process commands, the finite state machine and handlers are stubs
        self._protocol_fsm = ThreadSafeFSM(ProtocolState, ProtocolEvent,
                                           ProtocolEvent.ENTER,
                                           ProtocolEvent.EXIT)

        handlers = {
            ProtocolState.UNKNOWN: {
                (ProtocolEvent.ENTER, self._handler_state_change()),
                (ProtocolEvent.EXIT, self._handler_pass_through()),
                (ProtocolEvent.DISCOVER, self._handler_unknown_discover()),
            },
            ProtocolState.COMMAND: {
                (ProtocolEvent.ENTER, self._handler_state_change()),
                (ProtocolEvent.EXIT, self._handler_pass_through()),
                (ProtocolEvent.GET, self._handler_pass_through()),
            },
        }

        for state in handlers:
            for event, handler in handlers[state]:
                self._protocol_fsm.add_handler(state, event, handler)

        self._build_param_dict()
        self._build_command_dict()
        self._build_driver_dict()

        self._protocol_fsm.start(ProtocolState.UNKNOWN)

        self._chunker = StringChunker(self.sieve_function)

    @staticmethod
    def sieve_function(raw_data):
        """ The method that splits samples
        Over-ride sieve function to handle additional particles.
        """
        matchers = []
        return_list = []

        matchers.append(DoSampleParticle.regex_compiled())
        for matcher in matchers:
            for match in matcher.finditer(raw_data):
                return_list.append((match.start(), match.end()))

        return return_list

    def _build_command_dict(self):
        self._cmd_dict.add(Capability.DISCOVER,
                           display_name='Discover',
                           timeout=1)

    def _build_param_dict(self):
        pass
        # self._param_dict.add(Parameter.OPTODE,
        #                      r'OPTODE>(.*)</OPTODE',
        #                      lambda match: True if match.group(1) == 'yes' else False,
        #                      self._true_false_to_string,
        #                      type=ParameterDictType.BOOL,
        #                      display_name="Optode Attached",
        #                      description="Enable optode: (true | false)",
        #                      range={'True': True, 'False': False},
        #                      startup_param=True,
        #                      direct_access=True,
        #                      default_value=True,
        #                      visibility=ParameterDictVisibility.IMMUTABLE)
        # self._param_dict.add(Parameter.VOLT1,
        #                      r'ExtVolt1>(.*)</ExtVolt1',
        #                      lambda match: True if match.group(1) == 'yes' else False,
        #                      self._true_false_to_string,
        #                      type=ParameterDictType.BOOL,
        #                      display_name="Volt 1",
        #                      description="Enable external voltage 1: (true | false)",
        #                      range={'True': True, 'False': False},
        #                      startup_param=True,
        #                      direct_access=True,
        #                      default_value=True,
        #                      visibility=ParameterDictVisibility.IMMUTABLE)

    def _got_chunk(self, chunk, timestamp):
        """
        Over-ride sieve function to handle additional particles.
        The base class got_data has gotten a chunk from the chunker.  Pass it to extract_sample
        with the appropriate particle objects and REGEXes.
        """
        if self._extract_sample(DoSampleParticle,
                                DoSampleParticle.regex_compiled(), chunk,
                                timestamp):
            self._sampling = True
            return

    def _build_driver_dict(self):
        """
        Apparently VENDOR_SW_COMPATIBLE is required (TODO - move to the base class)
        """
        self._driver_dict.add(DriverDictKey.VENDOR_SW_COMPATIBLE, False)

    ####################
    # Command Handlers
    ####################
    def _handler_pass_through(self):
        pass

    def _handler_state_change(self):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    # noinspection PyMethodMayBeStatic
    def _handler_unknown_discover(self):
        next_state = ProtocolState.COMMAND
        return next_state, (next_state, None)

    def _handler_command_enter(self):
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)

    def _handler_command_get(self, *args, **kwargs):
        next_state, result = self._handler_get(*args, **kwargs)
        # TODO - need to find out why this doesn't match other handler return signatures:
        # TODO   (next_state, (next_state, result)
        return next_state, result