class SingleConnectionInstrumentDriver(InstrumentDriver):
    """
    Base class for instrument drivers with a single device connection.
    Provides connenction state logic for single connection drivers.
    """

    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 = InstrumentFSM(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.ACQUIRE_SAMPLE, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.START_AUTOSAMPLE, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.STOP_AUTOSAMPLE, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.TEST, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.CALIBRATE, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.EXECUTE_DIRECT, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.START_DIRECT, self._handler_connected_protocol_event
        )
        self._connection_fsm.add_handler(
            DriverConnectionState.CONNECTED, DriverEvent.STOP_DIRECT, self._handler_connected_protocol_event
        )

        # 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 StateError 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 StateError if command not allowed in current state        
        @throws ParameterError 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 StateError if command not allowed in current state
        @throws ConnectionError if the connection failed.
        """
        # Forward event and argument to the connection FSM.
        return self._connection_fsm.on_event(DriverEvent.CONNECT, *args, **kwargs)

    def disconnect(self, *args, **kwargs):
        """
        Disconnect from device via port agent / logger.
        @raises StateError 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)

    #############################################################
    # Commande and control interface.
    #############################################################

    def discover(self, *args, **kwargs):
        """
        Determine initial state upon establishing communications.
        @param timeout=timeout Optional command timeout.        
        @retval Current device state.
        @raises TimeoutError if could not wake device.
        @raises StateError if command not allowed in current state or if
        device state not recognized.
        @raises NotImplementedError 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(self, *args, **kwargs):
        """
        Retrieve device parameters.
        @param args[0] DriverParameter.ALL or a list of parameters to retrive.
        @retval parameter : value dict.
        @raises ParameterError if missing or invalid get parameters.
        @raises StateError if command not allowed in current state
        @raises NotImplementedError 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(self, *args, **kwargs):
        """
        Set device parameters.
        @param args[0] parameter : value dict of parameters to set.
        @param timeout=timeout Optional command timeout.
        @raises ParameterError if missing or invalid set parameters.
        @riases TimeoutError if could not wake device or no response.
        @raises ProtocolError if set command not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError 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_acquire_sample(self, *args, **kwargs):
        """
        Poll for a sample.
        @param timeout=timeout Optional command timeout.        
        @ retval Device sample dict.
        @riases TimeoutError if could not wake device or no response.
        @raises ProtocolError if acquire command not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                        
        """
        # Forward event and argument to the protocol FSM.
        return self._connection_fsm.on_event(DriverEvent.ACQUIRE_SAMPLE, DriverEvent.ACQUIRE_SAMPLE, *args, **kwargs)

    def execute_start_autosample(self, *args, **kwargs):
        """
        Switch to autosample mode.
        @param timeout=timeout Optional command timeout.        
        @riases TimeoutError if could not wake device or no response.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                        
        """
        # Forward event and argument to the protocol FSM.
        return self._connection_fsm.on_event(
            DriverEvent.START_AUTOSAMPLE, DriverEvent.START_AUTOSAMPLE, *args, **kwargs
        )

    def execute_stop_autosample(self, *args, **kwargs):
        """
        Leave autosample mode.
        @param timeout=timeout Optional command timeout.        
        @riases TimeoutError if could not wake device or no response.
        @raises ProtocolError if stop command not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                        
         """
        # Forward event and argument to the protocol FSM.
        return self._connection_fsm.on_event(DriverEvent.STOP_AUTOSAMPLE, DriverEvent.STOP_AUTOSAMPLE, *args, **kwargs)

    def execute_test(self, *args, **kwargs):
        """
        Execute device tests.
        @param timeout=timeout Optional command timeout (for wakeup only --
        device specific timeouts for internal test commands).
        @riases TimeoutError if could not wake device or no response.
        @raises ProtocolError if test commands not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                        
        """
        # Forward event and argument to the protocol FSM.
        return self._connection_fsm.on_event(DriverEvent.TEST, DriverEvent.TEST, *args, **kwargs)

    def execute_calibrate(self, *args, **kwargs):
        """
        Execute device calibration.
        @param timeout=timeout Optional command timeout (for wakeup only --
        device specific timeouts for internal calibration commands).
        @riases TimeoutError if could not wake device or no response.
        @raises ProtocolError if test commands not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                        
        """
        # Forward event and argument to the protocol FSM.
        return self._connection_fsm.on_event(DriverEvent.CALIBRATE, DriverEvent.CALIBRATE, *args, **kwargs)

    def execute_start_direct_access(self, *args, **kwargs):
        """
        Switch to direct access mode.
        @raises TimeoutError if could not wake device or no response.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                
        """
        return self._connection_fsm.on_event(DriverEvent.START_DIRECT, DriverEvent.START_DIRECT, *args, **kwargs)

    def execute_direct_access(self, *args, **kwargs):
        """
        output direct access data to device.
        @raises TimeoutError if could not wake device or no response.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                
        """
        return self._connection_fsm.on_event(DriverEvent.EXECUTE_DIRECT, DriverEvent.EXECUTE_DIRECT, *args, **kwargs)

    def execute_stop_direct_access(self, *args, **kwargs):
        """
        Leave direct access mode.
        @raises TimeoutError if could not wake device or no response.
        @raises ProtocolError if stop command not recognized.
        @raises StateError if command not allowed in current state.
        @raises NotImplementedError if not implemented by subclass.                
        """
        return self._connection_fsm.on_event(DriverEvent.STOP_DIRECT, DriverEvent.STOP_DIRECT, *args, **kwargs)

    ########################################################################
    # Resource query interface.
    ########################################################################

    def get_current_state(self):
        """
        Return current device state. For single connection devices, return
        a single connection state if not connected, and protocol state if connected.
        """
        connection_state = self._connection_fsm.get_current_state()
        if connection_state == DriverConnectionState.CONNECTED:
            return self._protocol.get_current_state()
        else:
            return connection_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 Parameter error if missing or invalid param dict.        
        """
        next_state = None
        result = None

        # Get the required param dict.
        try:
            config = args[0]

        except IndexError:
            raise ParameterError("Missing comms config parameter.")

        # Verify dict and construct connection client.
        try:
            addr = config["addr"]
            port = config["port"]

            if isinstance(addr, str) and isinstance(port, int) and len(addr) > 0:
                self._connection = LoggerClient(addr, port)
                next_state = DriverConnectionState.DISCONNECTED

            else:
                raise ParameterError("Invalid comms config dict.")

        except (TypeError, KeyError):
            raise ParameterError("Invalid comms config dict.")

        return (next_state, result)

    ########################################################################
    # Disconnected handlers.
    ########################################################################

    def _handler_disconnected_enter(self, *args, **kwargs):
        """
        Enter disconnected state.
        """
        # Send state change event to agent.
        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 Parameter error if missing or invalid param dict.
        """
        next_state = None
        result = None

        # Get required config param dict.
        try:
            config = args[0]

        except IndexError:
            raise ParameterError("Missing comms config parameter.")

        # Verify configuration dict, and update connection if possible.
        try:
            addr = config["addr"]
            port = config["port"]

            if isinstance(addr, str) and isinstance(port, int) and len(addr) > 0:
                self._connection = LoggerClient(addr, port)

            else:
                raise ParameterError("Invalid comms config dict.")

        except (TypeError, KeyError):
            raise ParameterError("Invalid comms config dict.")

        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 ConnectionError if the attempt to connect failed.
        """
        next_state = None
        result = None

        self._build_protocol()
        self._connection.init_comms(self._protocol.got_data)
        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._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
        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)

    ########################################################################
    # Helpers.
    ########################################################################

    def _build_protocol(self):
        """
        Construct device specific single connection protocol FSM.
        Overridden in device specific subclasses.
        """
        pass
Esempio n. 2
0
class SBE16Protocol(CommandResponseInstrumentProtocol):
    """
    Instrument protocol class for SBE16 driver.
    Subclasses CommandResponseInstrumentProtocol
    """
    def __init__(self, prompts, newline, driver_event):
        """
        SBE16Protocol constructor.
        @param prompts A BaseEnum class containing instrument prompts.
        @param newline The SBE16 newline.
        @param driver_event Driver process event callback.
        """
        # Construct protocol superclass.
        CommandResponseInstrumentProtocol.__init__(self, prompts, newline, driver_event)
        
        # Build SBE16 protocol state machine.
        self._protocol_fsm = InstrumentFSM(SBE16ProtocolState, SBE16ProtocolEvent,
                            SBE16ProtocolEvent.ENTER, SBE16ProtocolEvent.EXIT)

        # Add event handlers for protocol state machine.
        self._protocol_fsm.add_handler(SBE16ProtocolState.UNKNOWN, SBE16ProtocolEvent.ENTER, self._handler_unknown_enter)
        self._protocol_fsm.add_handler(SBE16ProtocolState.UNKNOWN, SBE16ProtocolEvent.EXIT, self._handler_unknown_exit)
        self._protocol_fsm.add_handler(SBE16ProtocolState.UNKNOWN, SBE16ProtocolEvent.DISCOVER, self._handler_unknown_discover)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.ENTER, self._handler_command_enter)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.EXIT, self._handler_command_exit)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.ACQUIRE_SAMPLE, self._handler_command_acquire_sample)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.START_AUTOSAMPLE, self._handler_command_start_autosample)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.GET, self._handler_command_autosample_test_get)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.SET, self._handler_command_set)
        self._protocol_fsm.add_handler(SBE16ProtocolState.COMMAND, SBE16ProtocolEvent.TEST, self._handler_command_test)
        self._protocol_fsm.add_handler(SBE16ProtocolState.AUTOSAMPLE, SBE16ProtocolEvent.ENTER, self._handler_autosample_enter)
        self._protocol_fsm.add_handler(SBE16ProtocolState.AUTOSAMPLE, SBE16ProtocolEvent.EXIT, self._handler_autosample_exit)
        self._protocol_fsm.add_handler(SBE16ProtocolState.AUTOSAMPLE, SBE16ProtocolEvent.GET, self._handler_command_autosample_test_get)
        self._protocol_fsm.add_handler(SBE16ProtocolState.AUTOSAMPLE, SBE16ProtocolEvent.STOP_AUTOSAMPLE, self._handler_autosample_stop_autosample)
        self._protocol_fsm.add_handler(SBE16ProtocolState.TEST, SBE16ProtocolEvent.ENTER, self._handler_test_enter)
        self._protocol_fsm.add_handler(SBE16ProtocolState.TEST, SBE16ProtocolEvent.EXIT, self._handler_test_exit)
        self._protocol_fsm.add_handler(SBE16ProtocolState.TEST, SBE16ProtocolEvent.RUN_TEST, self._handler_test_run_tests)
        self._protocol_fsm.add_handler(SBE16ProtocolState.TEST, SBE16ProtocolEvent.GET, self._handler_command_autosample_test_get)
        self._protocol_fsm.add_handler(SBE16ProtocolState.DIRECT_ACCESS, SBE16ProtocolEvent.ENTER, self._handler_direct_access_enter)
        self._protocol_fsm.add_handler(SBE16ProtocolState.DIRECT_ACCESS, SBE16ProtocolEvent.EXIT, self._handler_direct_access_exit)

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

        # Add build handlers for device commands.
        self._add_build_handler('ds', self._build_simple_command)
        self._add_build_handler('dc', self._build_simple_command)
        self._add_build_handler('ts', self._build_simple_command)
        self._add_build_handler('startnow', self._build_simple_command)
        self._add_build_handler('stop', self._build_simple_command)
        self._add_build_handler('tc', self._build_simple_command)
        self._add_build_handler('tt', self._build_simple_command)
        self._add_build_handler('tp', self._build_simple_command)
        self._add_build_handler('set', self._build_set_command)

        # Add response handlers for device commands.
        self._add_response_handler('ds', self._parse_dsdc_response)
        self._add_response_handler('dc', self._parse_dsdc_response)
        self._add_response_handler('ts', self._parse_ts_response)
        self._add_response_handler('set', self._parse_set_response)
        self._add_response_handler('tc', self._parse_test_response)
        self._add_response_handler('tt', self._parse_test_response)
        self._add_response_handler('tp', self._parse_test_response)

       # Add sample handlers.
        self._sample_pattern = r'^#? *(-?\d+\.\d+), *(-?\d+\.\d+), *(-?\d+\.\d+)'
        self._sample_pattern += r'(, *(-?\d+\.\d+))?(, *(-?\d+\.\d+))?'
        self._sample_pattern += r'(, *(\d+) +([a-zA-Z]+) +(\d+), *(\d+):(\d+):(\d+))?'
        self._sample_pattern += r'(, *(\d+)-(\d+)-(\d+), *(\d+):(\d+):(\d+))?'        
        self._sample_regex = re.compile(self._sample_pattern)

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

    ########################################################################
    # 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 be COMMAND or AUTOSAMPLE.
        @retval (next_state, result), (SBE16ProtocolState.COMMAND or
        SBE16State.AUTOSAMPLE, None) if successful.
        @throws TimeoutError if the device cannot be woken.
        @throws ProtocolError if the device response does not correspond to
        an expected state.
        """
        next_state = None
        result = None
        
        # Wakeup the device with timeout if passed.
        timeout = kwargs.get('timeout', SBE16_TIMEOUT)
        prompt = self._wakeup(timeout)
        prompt = self._wakeup(timeout)
        
        # Set the state to change.
        # Raise if the prompt returned does not match command or autosample.
        if prompt == SBE16Prompt.COMMAND:
            next_state = SBE16ProtocolState.COMMAND
            result = SBE16ProtocolState.COMMAND
        elif prompt == SBE16Prompt.AUTOSAMPLE:
            next_state = SBE16ProtocolState.AUTOSAMPLE
            result = SBE16ProtocolState.AUTOSAMPLE
        else:
            raise ProtocolError('Failure to recognzie device state.')
            
        return (next_state, result)

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

    def _handler_command_enter(self, *args, **kwargs):
        """
        Enter command state.
        @throws TimeoutError if the device cannot be woken.
        @throws ProtocolError if the update commands and not recognized.
        """
        # Command device to update parameters and send a config change event.
        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):
        """
        Perform a set command.
        @param args[0] parameter : value dict.
        @retval (next_state, result) tuple, (None, None).
        @throws ParameterError if missing set parameters, if set parameters not ALL and
        not a dict, or if paramter can't be properly formatted.
        @throws TimeoutError if device cannot be woken for set command.
        @throws ProtocolError if set command could not be built or misunderstood.
        """
        next_state = None
        result = None

        # Retrieve required parameter.
        # Raise if no parameter provided, or not a dict.
        try:
            params = args[0]
            
        except IndexError:
            raise ParameterError('Set command requires a parameter dict.')

        if not isinstance(params, dict):
            raise ParameterError('Set parameters not a dict.')
        
        # For each key, val in the dict, issue set command to device.
        # Raise if the command not understood.
        else:
            
            for (key, val) in params.iteritems():
                result = self._do_cmd_resp('set', key, val, **kwargs)
            self._update_params()
            
        return (next_state, result)

    def _handler_command_acquire_sample(self, *args, **kwargs):
        """
        Acquire sample from SBE16.
        @retval (next_state, result) tuple, (None, sample dict).        
        @throws TimeoutError if device cannot be woken for command.
        @throws ProtocolError if command could not be built or misunderstood.
        @throws SampleError if a sample could not be extracted from result.
        """
        next_state = None
        result = None

        result = self._do_cmd_resp('ts', *args, **kwargs)
        
        return (next_state, result)

    def _handler_command_start_autosample(self, *args, **kwargs):
        """
        Switch into autosample mode.
        @retval (next_state, result) tuple, (SBE16ProtocolState.AUTOSAMPLE,
        None) if successful.
        @throws TimeoutError if device cannot be woken for command.
        @throws ProtocolError if command could not be built or misunderstood.
        """
        next_state = None
        result = None

        # Assure the device is transmitting.
        if not self._param_dict.get(SBE16Parameter.TXREALTIME):
            self._do_cmd_resp('set', SBE16Parameter.TXREALTIME, True, **kwargs)
        
        # Issue start command and switch to autosample if successful.
        self._do_cmd_no_resp('startnow', *args, **kwargs)
                
        next_state = SBE16ProtocolState.AUTOSAMPLE        
        
        return (next_state, result)

    def _handler_command_test(self, *args, **kwargs):
        """
        Switch to test state to perform instrument tests.
        @retval (next_state, result) tuple, (SBE16ProtocolState.TEST, None).
        """
        next_state = None
        result = None

        next_state = SBE16ProtocolState.TEST
        
        return (next_state, result)

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

    def _handler_autosample_enter(self, *args, **kwargs):
        """
        Enter autosample state.
        """
        # 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.
        """
        pass

    def _handler_autosample_stop_autosample(self, *args, **kwargs):
        """
        Stop autosample and switch back to command mode.
        @retval (next_state, result) tuple, (SBE16ProtocolState.COMMAND,
        None) if successful.
        @throws TimeoutError if device cannot be woken for command.
        @throws ProtocolError if command misunderstood or
        incorrect prompt received.
        """
        next_state = None
        result = None

        # Wake up the device, continuing until autosample prompt seen.
        timeout = kwargs.get('timeout', SBE16_TIMEOUT)
        self._wakeup_until(timeout, SBE16Prompt.AUTOSAMPLE)

        # Issue the stop command.
        self._do_cmd_resp('stop', *args, **kwargs)        
        
        # Prompt device until command prompt is seen.
        self._wakeup_until(timeout, SBE16Prompt.COMMAND)
        
        next_state = SBE16ProtocolState.COMMAND

        return (next_state, result)
        
    ########################################################################
    # Common handlers.
    ########################################################################

    def _handler_command_autosample_test_get(self, *args, **kwargs):
        """
        Get device parameters from the parameter dict.
        @param args[0] list of parameters to retrieve, or DriverParameter.ALL.
        @throws ParameterError if missing or invalid parameter.
        """
        next_state = None
        result = None

        # Retrieve the required parameter, raise if not present.
        try:
            params = args[0]
           
        except IndexError:
            raise ParameterError('Get command requires a parameter list or tuple.')

        # If all params requested, retrieve config.
        if params == DriverParameter.ALL:
            result = self._param_dict.get_config()
                    
        # If not all params, confirm a list or tuple of params to retrieve.
        # Raise if not a list or tuple.
        # Retireve each key in the list, raise if any are invalid.
        else:
            if not isinstance(params, (list, tuple)):
                raise ParameterError('Get argument not a list or tuple.')
            result = {}
            for key in params:
                try:
                    val = self._param_dict.get(key)
                    result[key] = val

                except KeyError:
                    raise ParameterError(('%s is not a valid parameter.' % key))
            
        return (next_state, result)

    ########################################################################
    # Test handlers.
    ########################################################################

    def _handler_test_enter(self, *args, **kwargs):
        """
        Enter test state. Setup the secondary call to run the tests.
        """
        # Tell driver superclass to send a state change event.
        # Superclass will query the state.        
        self._driver_event(DriverAsyncEvent.STATE_CHANGE)
        
        # Forward the test event again to run the test handler and
        # switch back to command mode afterward.
        Timer(1, lambda: self._protocol_fsm.on_event(SBE16ProtocolEvent.RUN_TEST)).start()
    
    def _handler_test_exit(self, *args, **kwargs):
        """
        Exit test state.
        """
        pass

    def _handler_test_run_tests(self, *args, **kwargs):
        """
        Run test routines and validate results.
        @throws TimeoutError if device cannot be woken for command.
        @throws ProtocolError if command misunderstood or
        incorrect prompt received.
        """
        next_state = None
        result = None

        tc_pass = False
        tt_pass = False
        tp_pass = False
        tc_result = None
        tt_result = None
        tp_result = None

        test_result = {}

        try:
            tc_pass, tc_result = self._do_cmd_resp('tc', timeout=200)
            tt_pass, tt_result = self._do_cmd_resp('tt', timeout=200)
            tp_pass, tp_result = self._do_cmd_resp('tp', timeout=200)
        
        except Exception as e:
            test_result['exception'] = e
            test_result['message'] = 'Error running instrument tests.'
        
        finally:
            test_result['cond_test'] = 'Passed' if tc_pass else 'Failed'
            test_result['cond_data'] = tc_result
            test_result['temp_test'] = 'Passed' if tt_pass else 'Failed'
            test_result['temp_data'] = tt_result
            test_result['pres_test'] = 'Passed' if tp_pass else 'Failed'
            test_result['pres_data'] = tp_result
            test_result['success'] = 'Passed' if (tc_pass and tt_pass and tp_pass) else 'Failed'
            
        self._driver_event(DriverAsyncEvent.TEST_RESULT, test_result)
        next_state = SBE16ProtocolState.COMMAND
 
        return (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)
    
    def _handler_direct_access_exit(self, *args, **kwargs):
        """
        Exit direct access state.
        """
        pass

    ########################################################################
    # Private helpers.
    ########################################################################
        
    def _send_wakeup(self):
        """
        Send a newline to attempt to wake the SBE16 device.
        """
        self._connection.send(SBE16_NEWLINE)
                
    def _update_params(self, *args, **kwargs):
        """
        Update the parameter dictionary. Wake the device then issue
        display status and display calibration commands. The parameter
        dict will match line output and udpate itself.
        @throws TimeoutError if device cannot be timely woken.
        @throws ProtocolError if ds/dc misunderstood.
        """

        
        # Get old param dict config.
        old_config = self._param_dict.get_config()
        
        # Issue display commands and parse results.
        timeout = kwargs.get('timeout', SBE16_TIMEOUT)
        self._do_cmd_resp('ds',timeout=timeout)
        self._do_cmd_resp('dc',timeout=timeout)
        
        # 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()
        if new_config != old_config:
            self._driver_event(DriverAsyncEvent.CONFIG_CHANGE)
        
    def _build_simple_command(self, cmd):
        """
        Build handler for basic SBE16 commands.
        @param cmd the simple sbe16 command to format.
        @retval The command to be sent to the device.
        """
        return cmd+SBE16_NEWLINE
    
    def _build_set_command(self, cmd, param, val):
        """
        Build handler for set commands. param=val followed by newline.
        String val constructed by param dict formatting function.
        @param param the parameter key to set.
        @param val the parameter value to set.
        @ retval The set command to be sent to the device.
        @throws ProtocolError if the parameter is not valid or
        if the formatting function could not accept the value passed.
        """
        try:
            str_val = self._param_dict.format(param, val)
            set_cmd = '%s=%s' % (param, str_val)
            set_cmd = set_cmd + SBE16_NEWLINE
            
        except KeyError:
            raise ParameterError('Unknown driver parameter %s' % param)
            
        return set_cmd

    def _parse_set_response(self, response, prompt):
        """
        Parse handler for set command.
        @param response command response string.
        @param prompt prompt following command response.        
        @throws ProtocolError if set command misunderstood.
        """
        if prompt != SBE16Prompt.COMMAND:
            raise ProtocolError('Set command not recognized: %s' % response)

    def _parse_dsdc_response(self, response, prompt):
        """
        Parse handler for dsdc commands.
        @param response command response string.
        @param prompt prompt following command response.        
        @throws ProtocolError if dsdc command misunderstood.
        """
        if prompt != SBE16Prompt.COMMAND:
            raise ProtocolError('dsdc command not recognized: %s.' % response)
            
        for line in response.split(SBE16_NEWLINE):
            self._param_dict.update(line)
        
    def _parse_ts_response(self, response, prompt):
        """
        Response handler for ts command.
        @param response command response string.
        @param prompt prompt following command response.
        @retval sample dictionary containig c, t, d values.
        @throws ProtocolError if ts command misunderstood.
        @throws InstrumentSampleError if response did not contain a sample
        """
        
        if prompt != SBE16Prompt.COMMAND:
            raise ProtocolError('ts command not recognized: %s', response)
        
        sample = None
        for line in response.split(SBE16_NEWLINE):
            sample = self._extract_sample(line, True)
            if sample:
                break
        
        if not sample:     
            raise SampleError('Response did not contain sample: %s' % repr(response))
            
        return sample
                
    def _parse_test_response(self, response, prompt):
        """
        Do minimal checking of test outputs.
        @param response command response string.
        @param promnpt prompt following command response.
        @retval tuple of pass/fail boolean followed by response
        """
        
        success = False
        lines = response.split()
        if len(lines)>2:
            data = lines[1:-1]
            bad_count = 0
            for item in data:
                try:
                    float(item)
                    
                except ValueError:
                    bad_count += 1
            
            if bad_count == 0:
                success = True
        
        return (success, response)        
                
    def got_data(self, data):
        """
        Callback for receiving new data from the device.
        """
        # Call the superclass to update line and promp buffers.
        CommandResponseInstrumentProtocol.got_data(self, data)

        # If in streaming mode, process the buffer for samples to publish.
        cur_state = self.get_current_state()
        if cur_state == SBE16ProtocolState.AUTOSAMPLE:
            if SBE16_NEWLINE in self._linebuf:
                lines = self._linebuf.split(SBE16_NEWLINE)
                self._linebuf = lines[-1]
                for line in lines:
                    self._extract_sample(line)                    
                
    def _extract_sample(self, line, publish=True):
        """
        Extract sample from a response line if present and publish to agent.
        @param line string to match for sample.
        @param publsih boolean to publish sample (default True).
        @retval Sample dictionary if present or None.
        """
        sample = None
        match = self._sample_regex.match(line)
        if match:
            sample = {}
            sample['t'] = [float(match.group(1))]
            sample['c'] = [float(match.group(2))]
            sample['p'] = [float(match.group(3))]

            # Driver timestamp.
            sample['time'] = [time.time()]
            sample['stream_name'] = 'ctd_parsed'

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

        return sample            
        
    def _build_param_dict(self):
        """
        Populate the parameter dictionary with SBE16 parameters.
        For each parameter key, add match stirng, match lambda function,
        and value formatting function for set commands.
        """
        # Add parameter handlers to parameter dict.        
        self._param_dict.add(SBE16Parameter.OUTPUTSAL,
                             r'(do not )?output salinity with each sample',
                             lambda match : False if match.group(1) else True,
                             self._true_false_to_string)
        self._param_dict.add(SBE16Parameter.OUTPUTSV,
                             r'(do not )?output sound velocity with each sample',
                             lambda match : False if match.group(1) else True,
                             self._true_false_to_string)
        self._param_dict.add(SBE16Parameter.NAVG,
                             r'number of samples to average = (\d+)',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
        self._param_dict.add(SBE16Parameter.SAMPLENUM,
                             r'samplenumber = (\d+), free = \d+',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
        self._param_dict.add(SBE16Parameter.INTERVAL,
                             r'sample interval = (\d+) seconds',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
        self._param_dict.add(SBE16Parameter.STORETIME,
                             r'(do not )?store time with each sample',
                             lambda match : False if match.group(1) else True,
                             self._true_false_to_string)
        self._param_dict.add(SBE16Parameter.TXREALTIME,
                             r'(do not )?transmit real-time data',
                             lambda match : False if match.group(1) else True,
                             self._true_false_to_string)
        self._param_dict.add(SBE16Parameter.SYNCMODE,
                             r'serial sync mode (enabled|disabled)',
                             lambda match : False if (match.group(1)=='disabled') else True,
                             self._true_false_to_string)
        self._param_dict.add(SBE16Parameter.SYNCWAIT,
                             r'wait time after serial sync sampling = (\d+) seconds',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
        self._param_dict.add(SBE16Parameter.TCALDATE,
                             r'temperature: +((\d+)-([a-zA-Z]+)-(\d+))',
                             lambda match : self._string_to_date(match.group(1), '%d-%b-%y'),
                             self._date_to_string)
        self._param_dict.add(SBE16Parameter.TA0,
                             r' +TA0 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.TA1,
                             r' +TA1 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.TA2,
                             r' +TA2 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.TA3,
                             r' +TA3 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CCALDATE,
                             r'conductivity: +((\d+)-([a-zA-Z]+)-(\d+))',
                             lambda match : self._string_to_date(match.group(1), '%d-%b-%y'),
                             self._date_to_string)
        self._param_dict.add(SBE16Parameter.CG,
                             r' +G = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CH,
                             r' +H = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CI,
                             r' +I = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CJ,
                             r' +J = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.WBOTC,
                             r' +WBOTC = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CTCOR,
                             r' +CTCOR = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.CPCOR,
                             r' +CPCOR = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PCALDATE,
                             r'pressure .+ ((\d+)-([a-zA-Z]+)-(\d+))',
                             lambda match : self._string_to_date(match.group(1), '%d-%b-%y'),
                             self._date_to_string)
        self._param_dict.add(SBE16Parameter.PA0,
                             r' +PA0 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PA1,
                             r' +PA1 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PA2,
                             r' +PA2 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCA0,
                             r' +PTCA0 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCA1,
                             r' +PTCA1 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCA2,
                             r' +PTCA2 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCB0,
                             r' +PTCSB0 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCB1,
                             r' +PTCSB1 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.PTCB2,
                             r' +PTCSB2 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.POFFSET,
                             r' +POFFSET = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.RCALDATE,
                             r'rtc: +((\d+)-([a-zA-Z]+)-(\d+))',
                             lambda match : self._string_to_date(match.group(1), '%d-%b-%y'),
                             self._date_to_string)
        self._param_dict.add(SBE16Parameter.RTCA0,
                             r' +RTCA0 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.RTCA1,
                             r' +RTCA1 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
        self._param_dict.add(SBE16Parameter.RTCA2,
                             r' +RTCA2 = (-?\d.\d\d\d\d\d\de[-+]\d\d)',
                             lambda match : float(match.group(1)),
                             self._float_to_string)
    

    ########################################################################
    # Static helpers to format set commands.
    ########################################################################

    @staticmethod
    def _true_false_to_string(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 ParameterError if value not a bool.
        """
        
        if not isinstance(v,bool):
            raise ParameterError('Value %s is not a bool.' % str(v))
        if v:
            return 'y'
        else:
            return 'n'

    @staticmethod
    def _int_to_string(v):
        """
        Write an int value to string formatted for sbe16 set operations.
        @param v An int val.
        @retval an int string formatted for sbe16 set operations.
        @throws ParameterError if value not an int.
        """
        
        if not isinstance(v,int):
            raise ParameterError('Value %s is not an int.' % str(v))
        else:
            return '%i' % v

    @staticmethod
    def _float_to_string(v):
        """
        Write a float value to string formatted for sbe16 set operations.
        @param v A float val.
        @retval a float string formatted for sbe16 set operations.
        @throws ParameterError if value is not a float.
        """

        if not isinstance(v,float):
            raise ParameterError('Value %s is not a float.' % v)
        else:
            return '%e' % v

    @staticmethod
    def _date_to_string(v):
        """
        Write a date tuple to string formatted for sbe16 set operations.
        @param v a date tuple: (day,month,year).
        @retval A date string formatted for sbe16 set operations.
        @throws ParameterError if date tuple is not valid.
        """

        if not isinstance(v,(list,tuple)):
            raise ParameterError('Value %s is not a list, tuple.' % str(v))
        
        if not len(v)==3:
            raise ParameterError('Value %s is not length 3.' % str(v))
        
        months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep',
                  'Oct','Nov','Dec']
        day = v[0]
        month = v[1]
        year = v[2]
        
        if len(str(year)) > 2:
            year = int(str(year)[-2:])
        
        if not isinstance(day,int) or day < 1 or day > 31:
            raise ParameterError('Value %s is not a day of month.' % str(day))
        
        if not isinstance(month,int) or month < 1 or month > 12:
            raise ParameterError('Value %s is not a month.' % str(month))

        if not isinstance(year,int) or year < 0 or year > 99:
            raise ParameterError('Value %s is not a 0-99 year.' % str(year))
        
        return '%02i-%s-%02i' % (day,months[month-1],year)

    @staticmethod
    def _string_to_date(datestr,fmt):
        """
        Extract a date tuple from an sbe16 date string.
        @param str a string containing date information in sbe16 format.
        @retval a date tuple.
        @throws ParameterError if datestr cannot be formatted to
        a date.
        """
        if not isinstance(datestr,str):
            raise ParameterError('Value %s is not a string.' % str(datestr))
        try:
            date_time = time.strptime(datestr,fmt)
            date = (date_time[2],date_time[1],date_time[0])

        except ValueError:
            raise ParameterError('Value %s could not be formatted to a date.' % str(datestr))
                        
        return date
Esempio n. 3
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.
    @todo Check for valid state transitions and handle requests appropriately
    possibly using better exceptions from the fsm.on_event() method
    """
    
    
    def __init__(self, callback=None):
        CommandResponseInstrumentProtocol.__init__(self, callback, Prompt, "\n")
        
        self._fsm = InstrumentFSM(State, Event, Event.ENTER_STATE,
                                  Event.EXIT_STATE,
                                  InstErrorCode.UNHANDLED_EVENT)
        self._fsm.add_handler(State.COMMAND_MODE, Event.COMMAND,
                              self._handler_command_command)
        self._fsm.add_handler(State.COMMAND_MODE, Event.GET,
                              self._handler_command_get)    
        self._fsm.add_handler(State.COMMAND_MODE, Event.SET,
                              self._handler_command_set)
        self._fsm.add_handler(State.AUTOSAMPLE_MODE, Event.BREAK,
                              self._handler_autosample_break)
        self._fsm.add_handler(State.AUTOSAMPLE_MODE, Event.STOP,
                              self._handler_autosample_stop)
        self._fsm.add_handler(State.AUTOSAMPLE_MODE, Event.RESET,
                              self._handler_reset)
        self._fsm.add_handler(State.AUTOSAMPLE_MODE, Event.COMMAND,
                              self._handler_autosample_command)
        self._fsm.add_handler(State.POLL_MODE, Event.AUTOSAMPLE,
                              self._handler_poll_autosample)
        self._fsm.add_handler(State.POLL_MODE, Event.RESET,
                              self._handler_reset)
        self._fsm.add_handler(State.POLL_MODE, Event.SAMPLE,
                              self._handler_poll_sample)
        self._fsm.add_handler(State.POLL_MODE, Event.COMMAND,
                              self._handler_poll_command)
        self._fsm.add_handler(State.UNKNOWN, Event.INITIALIZE,
                              self._handler_initialize)
        self._fsm.start(State.UNKNOWN)

        self._add_build_handler(Command.SET, self._build_set_command)
        self._add_build_handler(Command.GET, self._build_param_fetch_command)
        self._add_build_handler(Command.SAVE, self._build_exec_command)
        self._add_build_handler(Command.EXIT, self._build_exec_command)
        self._add_build_handler(Command.EXIT_AND_RESET, self._build_exec_command)
        self._add_build_handler(Command.AUTOSAMPLE, self._build_control_command)
        self._add_build_handler(Command.RESET, self._build_control_command)
        self._add_build_handler(Command.BREAK, self._build_control_command)
        self._add_build_handler(Command.SAMPLE, self._build_control_command)
        self._add_build_handler(Command.STOP, self._build_control_command)

        
        self._add_response_handler(Command.SET, self._parse_set_response)
        # self._add_response_handler(Command.GET, self._parse_get_response)

        self._add_param_dict(Parameter.TELBAUD,
                             r'Telemetry Baud Rate:\s+(\d+) bps',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
        
        self._add_param_dict(Parameter.MAXRATE,
                             r'Maximum Frame Rate:\s+(\d+) Hz',
                             lambda match : int(match.group(1)),
                             self._int_to_string)
                
    # The normal interface for a protocol. These should drive the FSM
    # transitions as they get things done.
    def get(self, *args, **kwargs):
        """ Get the given parameters from the instrument
        
        @param params The parameter values to get
        @retval Result of FSM event handle, hould be a dict of parameters and values
        @throws InstrumentProtocolException On invalid parameter
        """
        # Parameters checked in Handler
        result = self._fsm.on_event(Event.GET, *args, **kwargs)
        if result == None:
            raise InstrumentProtocolException(InstErrorCode.INCORRECT_STATE)
        assert (isinstance(result, dict))
        return result
   
    def set(self, *args, **kwargs):
        """ Set the given parameters on the instrument
        
        @param params The dict of parameters and values to set
        @retval result of FSM event handle
        @throws InstrumentProtocolException On invalid parameter
        """
        # Parameters checked in handler
        result = self._fsm.on_event(Event.SET, *args, **kwargs)
        if result == None:
            raise InstrumentProtocolException(InstErrorCode.INCORRECT_STATE)
        assert(isinstance(result, dict))
        return result
    
    def execute_save(self, *args, **kwargs):
        """ Execute the save command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        kwargs.update({KwargsKey.COMMAND:Command.SAVE})
        return self._fsm.on_event(Event.COMMAND, *args, **kwargs)
        
    def execute_exit(self, *args, **kwargs):
        """ Execute the exit command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        kwargs.update({KwargsKey.COMMAND:Command.EXIT})
        return self._fsm.on_event(Event.COMMAND, *args, **kwargs)
        
    def execute_exit_and_reset(self, *args, **kwargs):
        """ Execute the exit and reset command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        kwargs.update({KwargsKey.COMMAND:Command.EXIT_AND_RESET})
        return self._fsm.on_event(Event.COMMAND, *args, **kwargs)
    
    def execute_poll(self, *args, **kwargs):
        """ Execute the poll command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        kwargs.update({KwargsKey.COMMAND:Command.POLL})
        return self._fsm.on_event(Event.COMMAND, *args, **kwargs)
    
    def execute_reset(self, *args, **kwargs):
        """ Execute the reset command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        return self._fsm.on_event(Event.RESET, *args, **kwargs)
    
    def execute_break(self, *args, **kwargs):
        """ Execute the break command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        return self._fsm.on_event(Event.BREAK, *args, **kwargs)
    
    def execute_stop(self, *args, **kwargs):
        """ Execute the stop command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        return self._fsm.on_event(Event.STOP, *args, **kwargs)
    
    def execute_autosample(self, *args, **kwargs):
        """ Execute the autosample command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        return self._fsm.on_event(Event.AUTOSAMPLE, *args, **kwargs) 
    
    def execute_sample(self, *args, **kwargs):
        """ Execute the sample command

        @retval None if nothing was done, otherwise result of FSM event handle
        @throws InstrumentProtocolException On invalid command or missing
        """
        return self._fsm.on_event(Event.SAMPLE, *args, **kwargs)
        
    def get_config(self):
        """ 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
        @throws InstrumentProtocolException On invalid parameter
        """
        result = self.get([Parameter.TELBAUD, Parameter.MAXRATE])
        assert (isinstance(result, dict))
        assert (result.has_key(Parameter.TELBAUD))
        assert (result.has_key(Parameter.MAXRATE))
        return result
        
    def restore_config(self, config=None):
        """ Apply a complete configuration.
        
        In this instrument, it is simply a compound set that must contain all
        of the parameters.
        @throws InstrumentProtocolException on missing or bad config
        """
        if (config == None):
            raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
        
        if ((config.has_key(Parameter.TELBAUD))
            and (config.has_key(Parameter.MAXRATE))):  
            assert (isinstance(config, dict))
            assert (len(config) == 2)
            self.set(config)
        else:
            raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
        
    def get_status(self):
        """
        Get the current state of the state machine as the instrument
        doesnt maintain a status beyond its configuration and its active mode
        
        @retval Something from the State enum
        """
        return self._fsm.current_state()
    
    def initialize(self, *args, **kwargs):
        mi_logger.info('Initializing PAR sensor')
        self._fsm.on_event(Event.INITIALIZE, *args, **kwargs)

    ################
    # State handlers
    ################
    def _handler_initialize(self, *args, **kwargs):
        """Handle transition from UNKNOWN state to a known one.
        
        This method determines what state the device is in or gets it to a
        known state so that the instrument and protocol are in sync.
        @param params Parameters to pass to the state
        @retval return (next state, result)
        """
        next_state = None
        result = None
                
        # Break to command mode, then set next state to command mode
        if self._send_break(Command.BREAK):
            self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                    msg="Initialized, in command mode")            
            next_state = State.COMMAND_MODE
            
        return (next_state, result)
        
        
    def _handler_reset(self, *args, **kwargs):
        """Handle reset condition for all states.
        
        @param params Parameters to pass to the state
        @retval return (next state, result)
        """
        next_state = None
        result = None
        if (self._send_break(Command.RESET)):
            self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                    msg="Reset!")
            next_state = State.AUTOSAMPLE_MODE
            
        return (next_state, result)
        
    def _handler_autosample_break(self, *args, **kwargs):
        """Handle State.AUTOSAMPLE_MODE Event.BREAK
        
        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None
        
        if (self._send_break(Command.BREAK)):
            self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                    msg="Leaving auto sample!")
            next_state = State.COMMAND_MODE
        else:
            self.announce_to_driver(DriverAnnouncement.ERROR,
                                    error_code=InstErrorCode.HARDWARE_ERROR,
                                    msg="Could not break from autosample!")
            raise InstrumentProtocolException(InstErrorCode.HARDWARE_ERROR)
            
        return (next_state, result)
        
    def _handler_autosample_stop(self, *args, **kwargs):
        """Handle State.AUTOSAMPLE_MODE Event.STOP
        
        @param params Parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For hardware error
        """
        next_state = None
        result = None
        
        if (self._send_break(Command.STOP)):
            self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                    msg="Leaving auto sample!")
            next_state = State.POLL_MODE
        else:
            self.announce_to_driver(DriverAnnouncement.ERROR,
                                    error_code=InstErrorCode.HARDWARE_ERROR,
                                    msg="Could not stop autosample!")
            raise InstrumentProtocolException(InstErrorCode.HARDWARE_ERROR)
                
        return (next_state, result)

    def _handler_autosample_command(self, *args, **kwargs):
        """Handle State.AUTOSAMPLE_MODE Event.COMMAND transition
        
        @param params Dict with "command" enum and "params" of the parameters to
        pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid parameter
        """
        next_state = None
        result = None
                    
        cmd = kwargs.get(KwargsKey.COMMAND, None)

        if (cmd == Command.BREAK):
            result = self._fsm.on_event(Event.BREAK, *args, **kwargs)
        elif (cmd == Command.STOP):
            result = self._fsm.on_event(Event.STOP, *args, **kwargs)
        elif (cmd == Command.RESET):
            result = self._fsm.on_event(Event.RESET, *args, **kwargs)
        else:
            raise InstrumentProtocolException(InstErrorCode.INVALID_COMMAND)
        
        mi_logger.debug("next: %s, result: %s", next_state, result) 
        return (next_state, result)

    def _handler_command_command(self, *args, **kwargs):
        """Handle State.COMMAND_MODE Event.COMMAND transition
        
        @param params Dict with "command" enum and "params" of the parameters to
        pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid parameter
        """
        next_state = None
        result = None
        cmd = kwargs.get(KwargsKey.COMMAND, None)

        if cmd == Command.EXIT:
            result = self._do_cmd_no_resp(Command.EXIT, None)
            if result:
                self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                         msg="Starting auto sample")
                next_state = State.AUTOSAMPLE_MODE
            
        elif cmd == Command.EXIT_AND_RESET:
            result = self._do_cmd_no_resp(Command.EXIT_AND_RESET, None)
            if result:
                self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                         msg="Starting auto sample")
                next_state = State.AUTOSAMPLE_MODE
            
        elif cmd == Command.SAVE:
            result = self._do_cmd_no_resp(Command.SAVE, None)
        
        elif cmd == Command.POLL:
            try:
                kwargs.update({KwargsKey.COMMAND:Command.EXIT})
                result = self._fsm.on_event(Event.COMMAND, *args, **kwargs)
                result = self._fsm.on_event(Event.STOP, *args, **kwargs)
                result = self._fsm.on_event(Event.SAMPLE, *args, **kwargs)
                # result should have data, right?
                mi_logger.debug("Polled sample: %s", result)
                result = self._fsm.on_event(Event.AUTOSAMPLE, *args, **kwargs)
                result = self._fsm.on_event(Event.BREAK, *args, **kwargs)   
            except (InstrumentTimeoutException, InstrumentProtocolException) as e:
                if self._fsm.current_state == State.AUTOSAMPLE_MODE:
                    result = self._fsm.on_event(Event.BREAK, *args, **kwargs)
                elif (self._fsm.current_state == State.POLL_MODE):
                    result = self._fsm.on_event(Event.AUTOSAMPLE, *args, **kwargs)
                    result = self._fsm.on_event(Event.BREAK, *args, **kwargs)
        
        else:
            raise InstrumentProtocolException(InstErrorCode.INVALID_COMMAND)

        mi_logger.debug("next: %s, result: %s", next_state, result) 
        return (next_state, result)

    def _handler_command_get(self, params=None, *args, **kwargs):
        """Handle getting data from command mode
         
        @param params List of the parameters to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid parameter
        """
        next_state = None
        result = None
        result_vals = {}    
        
        if ((params == None) or (not isinstance(params, list))):
                raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
                
        for param in params:
            if not Parameter.has(param):
                raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
                break
            result_vals[param] = self._do_cmd_resp(Command.GET, param)
        result = result_vals
            
        mi_logger.debug("next: %s, result: %s", next_state, result) 
        return (next_state, result)

    def _handler_command_set(self, params, *args, **kwargs):
        """Handle setting data from command mode
         
        @param params Dict of the parameters and values to pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid parameter
        """
        next_state = None
        result = None
        result_vals = {}    
        
        if ((params == None) or (not isinstance(params, dict))):
            raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
        name_values = params
        for key in name_values.keys():
            if not Parameter.has(key):
                raise InstrumentProtocolException(InstErrorCode.INVALID_PARAMETER)
                break
            result_vals[key] = self._do_cmd_resp(Command.SET, key, name_values[key])
        """@todo raise a parameter error if there was a bad value"""
        result = result_vals
            
        mi_logger.debug("next: %s, result: %s", next_state, result) 
        return (next_state, result)

    def _handler_poll_sample(self, *args, **kwargs):
        """Handle State.POLL_MODE Event.SAMPLE
        
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid command
        """
        next_state = None
        result = None
        
        result = self._do_cmd_resp(Command.SAMPLE, None)

        self.announce_to_driver(DriverAnnouncement.DATA_RECEIVED,
                                msg=result)          
        return (next_state, result)

    def _handler_poll_autosample(self, *args, **kwargs):
        """Handle State.POLL_MODE Event.AUTOSAMPLE
        
        @retval return (success/fail code, next state, result)
        """
        next_state = None
        result = None
                
        if (self._do_cmd_no_resp(Command.AUTOSAMPLE, None)):
            self.announce_to_driver(DriverAnnouncement.STATE_CHANGE,
                                    msg="Starting auto sample")
            next_state = State.AUTOSAMPLE_MODE
                        
        return (next_state, result)
        
    def _handler_poll_command(self, *args, **kwargs):
        """Handle State.POLL_MODE Event.COMMAND transition
        
        @param params Dict with "command" enum and "params" of the parameters to
        pass to the state
        @retval return (next state, result)
        @throw InstrumentProtocolException For invalid command
        """
        next_state = None
        result = None
        result_vals = {} 
        
        cmd = kwargs.get(KwargsKey.COMMAND, None)
        
        if (cmd == Command.AUTOSAMPLE):
            result = self._fsm.on_event(Event.AUTOSAMPLE, *args, **kwargs)
        elif (cmd == Command.RESET):
            result = self._fsm.on_event(Event.RESET, *args, **kwargs)
        elif (cmd == Command.POLL):
            result = self._fsm.on_event(Event.SAMPLE, *args, **kwargs)
        else:
            raise InstrumentProtocolException(InstErrorCode.INVALID_COMMAND)
        
        mi_logger.debug("next: %s, result: %s", next_state, result) 
        return (next_state, result)

    ###################################################################
    # Builders
    ###################################################################
    def _build_set_command(self, cmd, param, value):
        """
        Build a command that is ready to send out to the instrument. Checks for
        valid parameter name, only handles one value at a time.
        
        @param cmd The command...in this case, Command.SET
        @param param The name of the parameter to set. From Parameter enum
        @param value The value to set for that parameter
        @retval Returns string ready for sending to instrument
        """
        # Check to make sure all parameters are valid up front
        assert Parameter.has(param)
        assert cmd == Command.SET
        return "%s %s %s%s" % (Command.SET, param, value, self.eoln)
        
    def _build_param_fetch_command(self, cmd, param):
        """
        Build a command to fetch the desired argument.
        
        @param cmd The command being used (Command.GET in this case)
        @param param The name of the parameter to fetch
        @retval Returns string ready for sending to instrument
        """
        assert Parameter.has(param)
        return "%s %s%s" % (Command.GET, param, self.eoln)
    
    def _build_exec_command(self, cmd, param):
        """
        Builder for simple commands

        @param cmd The command being used (Command.GET in this case)
        @param param The name of the parameter to fetch
        @retval Returns string ready for sending to instrument        
        """
        assert param == None
        return "%s%s" % (cmd, self.eoln)
    
    def _build_control_command(self, cmd, param):
        """ Send a quick control char command
        
        @param cmd The control character to send
        @param param Unused parameters
        @retval The string wit the complete command (1 char)
        """
        return cmd
    
    ##################################################################
    # 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
        """
        mi_logger.debug("Parsing SET response of %s with prompt %s",
                        response, prompt)
        if ((prompt != Prompt.COMMAND) or (response == Error.INVALID_COMMAND)):
            return InstErrorCode.SET_DEVICE_ERR
        
    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
        @retval return The numerical value of the parameter in the known units
        """
        pass
    
    ###################################################################
    # Helpers
    ###################################################################
    def _wakeup(self, timeout):
        """There is no wakeup sequence for this instrument"""
        pass
            
    def _send_break(self, break_char, timeout=30):
        """Break out of autosample mode.
        
        Issue the proper sequence of stuff to get the device out of autosample
        mode. The character used will result in a different end state. Ctrl-S
        goes to poll mode, Ctrl-C goes to command mode. Ctrl-R resets. 
        @param break_char The character to send to get out of autosample.
        Should be Event.STOP, Event.BREAK, or Event.RESET.
        @retval return True for success, Error for failure
        @throw InstrumentTimeoutException
        @throw InstrumentProtocolException
        """
        if not ((break_char == Command.BREAK)
            or (break_char == Command.STOP)
            or (break_char == Command.RESET)):
            return False

        mi_logger.debug("Sending break char %s", break_char)        
        # do the magic sequence of sending lots of characters really fast
        starttime = time.time()
        while True:
            self._do_cmd_no_resp(break_char, None)
            (prompt, result) = self._get_response(timeout)
            mi_logger.debug("Got prompt %s when trying to break",
                            prompt)
            if (prompt):                
                return True
            else:
                if time.time() > starttime + timeout:
                    raise InstrumentTimeoutException(InstErrorCode.TIMEOUT)
                    
        # catch all
        return False
    
    def _got_data(self, data):
        """ The comms object fires this when data is received
        
        @param data The chunk of data that was received
        """
        mi_logger.debug("*** Data received: %s, promptbuf: %s", data, self._promptbuf)
        CommandResponseInstrumentProtocol._got_data(self, data)
        
        # Only keep the latest characters in the prompt buffer.
        #if len(self._promptbuf)>7:
        #    self._promptbuf = self._promptbuf[-7:]
            
        # If we are streaming, process the line buffer for samples.
        if self._fsm.get_current_state() == State.AUTOSAMPLE_MODE:
            if self.eoln in self._linebuf:
                lines = self._linebuf.split(self.eoln)
                self._linebuf = lines[-1]
                for line in lines:
                    self.announce_to_driver(DriverAnnouncement.DATA_RECEIVED,
                                            msg=line)