예제 #1
0
    def test_get(self):
        """
        test getting values with expiration
        """
        # from mi.core.exceptions import InstrumentParameterExpirationException
        pd = ProtocolParameterDict()

        # No expiration, should work just fine
        pd.add('noexp', r'', None, None, expiration=None)
        pd.add('zeroexp', r'', None, None, expiration=0)
        pd.add('lateexp', r'', None, None, expiration=2)

        ###
        # Set and get with no expire
        ###
        pd.set_value('noexp', 1)
        self.assertEqual(pd.get('noexp'), 1)

        ###
        # Set and get with a 0 expire
        ###
        basetime = pd.get_current_timestamp()
        pd.set_value('zeroexp', 2)

        # We should fail because we are calculating exp against current time
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('zeroexp')

        # Should succeed because exp is calculated using basetime
        self.assertEqual(pd.get('zeroexp', basetime), 2)

        ###
        # Set and get with a delayed expire
        ###
        basetime = pd.get_current_timestamp()
        futuretime = pd.get_current_timestamp(3)
        self.assertGreater(futuretime - basetime, 3)

        pd.set_value('lateexp', 2)

        # Success because data is not expired
        self.assertEqual(pd.get('lateexp', basetime), 2)

        # Fail because data is expired (simulated three seconds from now)
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('lateexp', futuretime)
    def test_get(self):
        """
        test getting values with expiration
        """
        # from mi.core.exceptions import InstrumentParameterExpirationException
        pd = ProtocolParameterDict()

        # No expiration, should work just fine
        pd.add('noexp', r'', None, None, expiration=None)
        pd.add('zeroexp', r'', None, None, expiration=0)
        pd.add('lateexp', r'', None, None, expiration=2)

        ###
        # Set and get with no expire
        ###
        pd.set_value('noexp', 1)
        self.assertEqual(pd.get('noexp'), 1)

        ###
        # Set and get with a 0 expire
        ###
        basetime = pd.get_current_timestamp()
        pd.set_value('zeroexp', 2)

        # We should fail because we are calculating exp against current time
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('zeroexp')

        # Should succeed because exp is calculated using basetime
        self.assertEqual(pd.get('zeroexp', basetime), 2)

        ###
        # Set and get with a delayed expire
        ###
        basetime = pd.get_current_timestamp()
        futuretime = pd.get_current_timestamp(3)
        self.assertGreater(futuretime - basetime, 3)

        pd.set_value('lateexp', 2)

        # Success because data is not expired
        self.assertEqual(pd.get('lateexp', basetime), 2)

        # Fail because data is expired (simulated three seconds from now)
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('lateexp', futuretime)
예제 #3
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)
예제 #4
0
class TestUnitProtocolParameterDict(TestUnitStringsDict):
    """
    Test cases for instrument driver class. Functions in this class provide
    instrument driver unit tests and provide a tutorial on use of
    the driver interface.
    """

    __test__ = True

    @staticmethod
    def pick_byte2(input_val):
        """ Get the 2nd byte as an example of something tricky and
        arbitrary"""
        val = int(input_val) >> 8
        val &= 255
        return val

    def setUp(self):
        self.param_dict = ProtocolParameterDict()

        self.param_dict.add("foo",
                            r'.*foo=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=True,
                            startup_param=True,
                            default_value=10,
                            visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("bar",
                            r'.*bar=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=False,
                            startup_param=True,
                            default_value=15,
                            visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add(
            "baz",
            r'.*baz=(\d+).*',
            lambda match: int(match.group(1)),
            lambda x: str(x),
            direct_access=True,
            default_value=20,
            visibility=ParameterDictVisibility.DIRECT_ACCESS,
            get_timeout=30,
            set_timeout=40,
            display_name="Baz",
            description="The baz parameter",
            type=ParameterDictType.INT,
            units="nano-bazers",
            value_description="Should be an integer between 2 and 2000")
        self.param_dict.add(
            "bat",
            r'.*bat=(\d+).*',
            lambda match: int(match.group(1)),
            lambda x: str(x),
            startup_param=False,
            default_value=20,
            visibility=ParameterDictVisibility.READ_ONLY,
            get_timeout=10,
            set_timeout=20,
            display_name="Bat",
            description="The bat parameter",
            type=ParameterDictType.INT,
            units="nano-batbit",
            value_description="Should be an integer between 1 and 1000")
        self.param_dict.add("qux",
                            r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.READ_ONLY)
        self.param_dict.add("pho",
                            r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.IMMUTABLE)
        self.param_dict.add("dil",
                            r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.IMMUTABLE)
        self.param_dict.add(
            "qut",
            r'.*qut=(\d+).*',
            lambda match: int(match.group(1)),
            lambda x: str(x),
            direct_access=True,
            default_value=[10, 100],
            visibility=ParameterDictVisibility.DIRECT_ACCESS,
            expiration=1,
            get_timeout=10,
            set_timeout=20,
            display_name="Qut",
            description="The qut list parameter",
            type=ParameterDictType.LIST,
            units="nano-qutters",
            value_description=
            "Should be a 2-10 element list of integers between 2 and 2000")

        self.target_schema = {
            "bar": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": True,
                "value": {
                    "default": 15
                },
                "visibility": "READ_WRITE",
                "range": None,
            },
            "bat": {
                "description": "The bat parameter",
                "direct_access": False,
                "display_name": "Bat",
                "get_timeout": 10,
                "set_timeout": 20,
                "startup": False,
                "value": {
                    "default": 20,
                    "description": "Should be an integer between 1 and 1000",
                    "type": "int",
                    "units": "nano-batbit"
                },
                "visibility": "READ_ONLY",
                "range": None,
            },
            "baz": {
                "description": "The baz parameter",
                "direct_access": True,
                "display_name": "Baz",
                "get_timeout": 30,
                "set_timeout": 40,
                "startup": False,
                "value": {
                    "default": 20,
                    "description": "Should be an integer between 2 and 2000",
                    "type": "int",
                    "units": "nano-bazers"
                },
                "visibility": "DIRECT_ACCESS",
                "range": None,
            },
            "dil": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "IMMUTABLE",
                "range": None,
            },
            "foo": {
                "direct_access": True,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": True,
                "value": {
                    "default": 10
                },
                "visibility": "READ_WRITE",
                "range": None,
            },
            "pho": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "IMMUTABLE",
                "range": None,
            },
            "qut": {
                "description": "The qut list parameter",
                "direct_access": True,
                "display_name": "Qut",
                "get_timeout": 10,
                "set_timeout": 20,
                "startup": False,
                "value": {
                    "default": [10, 100],
                    "description":
                    "Should be a 2-10 element list of integers between 2 and 2000",
                    "type": "list",
                    "units": "nano-qutters"
                },
                "visibility": "DIRECT_ACCESS",
                "range": None,
            },
            "qux": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "READ_ONLY",
                "range": None,
            }
        }

        self.test_yaml = '''
        parameters: {
            qut: {
            description: "QutFileDesc",
            units: "QutFileUnits",
            value_description: "QutFileValueDesc",
            type: "QutFileType",
            display_name: "QutDisplay"
            },
            extra_param: {
            description: "ExtraFileDesc",
            units: "ExtraFileUnits",
            value_description: "ExtraFileValueDesc",
            type: "ExtraFileType"
            }
          }

        commands: {
          dummy: stuff
          }
        '''

    def test_get_direct_access_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_direct_access_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 3)
        self.assert_("foo" in result)
        self.assert_("baz" in result)
        self.assert_("qut" in result)

    def test_get_startup_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_startup_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("bar" in result)

    def test_set_default(self):
        """
        Test setting a default value
        """
        result = self.param_dict.get_config()
        self.assertEquals(result["foo"], None)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        self.param_dict.update("foo=1000")
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)

        self.assertRaises(ValueError, self.param_dict.set_default, "qux")

    def test_update_many(self):
        """
        Test updating of multiple variables from the same input
        """
        sample_input = """
foo=100
bar=200, baz=300
"""
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertNotEquals(self.param_dict.get("baz"), 300)
        result = self.param_dict.update_many(sample_input)
        log.debug("result: %s", result)
        self.assertEquals(result["foo"], True)
        self.assertEquals(result["bar"], True)
        self.assertEquals(result["baz"], True)
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)
        self.assertEquals(self.param_dict.get("baz"), 300)

    def test_update_specific_values(self):
        """
        test to verify we can limit update to a specific
        set of parameters
        """
        sample_input = "foo=100, bar=200"

        # First verify we can set both
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertTrue(self.param_dict.update(sample_input))
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Now let's only have it update 1 parameter with a name
        sample_input = "foo=200, bar=300"
        self.assertTrue(
            self.param_dict.update(sample_input, target_params="foo"))
        self.assertEquals(self.param_dict.get("foo"), 200)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Now let's only have it update 1 parameter using a list
        sample_input = "foo=300, bar=400"
        self.assertTrue(
            self.param_dict.update(sample_input, target_params=["foo"]))
        self.assertEquals(self.param_dict.get("foo"), 300)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Test our exceptions
        with self.assertRaises(KeyError):
            self.param_dict.update(sample_input, "key_does_not_exist")

        with self.assertRaises(InstrumentParameterException):
            self.param_dict.update(sample_input, {'bad': "key_does_not_exist"})

    def test_visibility_list(self):
        lst = self.param_dict.get_visibility_list(
            ParameterDictVisibility.READ_WRITE)
        lst.sort()
        self.assertEquals(lst, ["bar", "foo"])
        lst = self.param_dict.get_visibility_list(
            ParameterDictVisibility.DIRECT_ACCESS)
        lst.sort()
        self.assertEquals(lst, ["baz", "qut"])
        lst = self.param_dict.get_visibility_list(
            ParameterDictVisibility.READ_ONLY)
        lst.sort()
        self.assertEquals(lst, ["bat", "qux"])
        lst = self.param_dict.get_visibility_list(
            ParameterDictVisibility.IMMUTABLE)
        lst.sort()
        self.assertEquals(lst, ["dil", "pho"])

    def test_function_values(self):
        """
        Make sure we can add and update values with functions instead of patterns
        """

        self.param_dict.add_parameter(
            FunctionParameter("fn_foo",
                              self.pick_byte2,
                              lambda x: str(x),
                              direct_access=True,
                              startup_param=True,
                              value=1,
                              visibility=ParameterDictVisibility.READ_WRITE))
        self.param_dict.add_parameter(
            FunctionParameter(
                "fn_bar",
                lambda x: bool(x & 2),  # bit map example
                lambda x: str(x),
                direct_access=True,
                startup_param=True,
                value=False,
                visibility=ParameterDictVisibility.READ_WRITE))

        # check defaults just to be safe
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 1)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        self.param_dict.update(1005)  # just change first in list
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 3)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        # fn_bar does not get updated here
        result = self.param_dict.update_many(1205)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(len(result), 1)
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 4)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        # both are updated now
        result = self.param_dict.update_many(6)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(result['fn_bar'], True)
        self.assertEqual(len(result), 2)

        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 0)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, True)

    def test_mixed_pdv_types(self):
        """ Verify we can add different types of PDVs in one container """
        self.param_dict.add_parameter(
            FunctionParameter("fn_foo",
                              self.pick_byte2,
                              lambda x: str(x),
                              direct_access=True,
                              startup_param=True,
                              value=1,
                              visibility=ParameterDictVisibility.READ_WRITE))
        self.param_dict.add_parameter(
            RegexParameter("foo",
                           r'.*foo=(\d+).*',
                           lambda match: int(match.group(1)),
                           lambda x: str(x),
                           direct_access=True,
                           startup_param=True,
                           value=10,
                           visibility=ParameterDictVisibility.READ_WRITE))
        self.param_dict.add("bar",
                            r'.*bar=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=False,
                            startup_param=True,
                            value=15,
                            visibility=ParameterDictVisibility.READ_WRITE)

        self.assertEqual(self.param_dict.get("fn_foo"), 1)
        self.assertEqual(self.param_dict.get("foo"), 10)
        self.assertEqual(self.param_dict.get("bar"), 15)

    def test_base_update(self):
        pdv = Parameter("foo", lambda x: str(x), value=12)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update(1)
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), 1)

        # Its a base class...monkey see, monkey do
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), "foo=1")

    def test_regex_val(self):
        pdv = RegexParameter("foo",
                             r'.*foo=(\d+).*',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             value=12)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update(1)
        self.assertEqual(result, False)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), 1)

    def test_function_val(self):
        pdv = FunctionParameter("foo",
                                self.pick_byte2,
                                lambda x: str(x),
                                value=12)
        self.assertEqual(pdv.get_value(), 12)
        self.assertRaises(TypeError, pdv.update(1))
        result = pdv.update("1205")
        self.assertEqual(pdv.get_value(), 4)
        self.assertEqual(result, True)

    def test_set_init_value(self):
        result = self.param_dict.get("foo")
        self.assertEqual(result, None)
        self.param_dict.set_init_value("foo", 42)
        result = self.param_dict.get_init_value("foo")
        self.assertEqual(result, 42)

    def test_schema_generation(self):
        self.maxDiff = None
        result = self.param_dict.generate_dict()
        json_result = json.dumps(result, indent=4, sort_keys=True)
        log.debug("Expected: %s", self.target_schema)
        log.debug("Result: %s", json_result)
        self.assertEqual(result, self.target_schema)

    def test_empty_schema(self):
        self.param_dict = ProtocolParameterDict()
        result = self.param_dict.generate_dict()
        self.assertEqual(result, {})

    def test_bad_descriptions(self):
        self.param_dict._param_dict["foo"].description = None
        self.param_dict._param_dict["foo"].value = None
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_init_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_default_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.set_default, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_init_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_menu_path_read, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_submenu_read, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_menu_path_write, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_submenu_write, "foo")
        self.assertRaises(InstrumentParameterException, self.param_dict.format,
                          "foo", 1)
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_direct_access_list)
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.is_startup_param, "foo")

    def test_set(self):
        """
        Test a simple set of the parameter. Make sure the right values get
        called and the correct exceptions are raised.
        """
        new_param = FunctionParameter(
            "foo",
            self.pick_byte2,
            lambda x: str(x),
            direct_access=True,
            startup_param=True,
            value=1000,
            visibility=ParameterDictVisibility.READ_WRITE)
        self.assertEquals(new_param.get_value(), 1000)
        self.assertEquals(self.param_dict.get("foo"), None)
        # overwrites existing param
        self.param_dict.add_parameter(new_param)
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_value("foo", 2000)
        self.assertEquals(self.param_dict.get("foo"), 2000)

    def test_invalid_type(self):
        self.assertRaises(
            InstrumentParameterException,
            FunctionParameter,
            "fn_bar",
            lambda x: bool(x & 2),  # bit map example
            lambda x: str(x),
            direct_access=True,
            startup_param=True,
            value=False,
            type="bad_type",
            visibility=ParameterDictVisibility.READ_WRITE)

    def test_get(self):
        """
        test getting values with expiration
        """
        # from mi.core.exceptions import InstrumentParameterExpirationException
        pd = ProtocolParameterDict()

        # No expiration, should work just fine
        pd.add('noexp', r'', None, None, expiration=None)
        pd.add('zeroexp', r'', None, None, expiration=0)
        pd.add('lateexp', r'', None, None, expiration=2)

        ###
        # Set and get with no expire
        ###
        pd.set_value('noexp', 1)
        self.assertEqual(pd.get('noexp'), 1)

        ###
        # Set and get with a 0 expire
        ###
        basetime = pd.get_current_timestamp()
        pd.set_value('zeroexp', 2)

        # We should fail because we are calculating exp against current time
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('zeroexp')

        # Should succeed because exp is calculated using basetime
        self.assertEqual(pd.get('zeroexp', basetime), 2)

        ###
        # Set and get with a delayed expire
        ###
        basetime = pd.get_current_timestamp()
        futuretime = pd.get_current_timestamp(3)
        self.assertGreater(futuretime - basetime, 3)

        pd.set_value('lateexp', 2)

        # Success because data is not expired
        self.assertEqual(pd.get('lateexp', basetime), 2)

        # Fail because data is expired (simulated three seconds from now)
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('lateexp', futuretime)

    def test_regex_flags(self):
        pdv = RegexParameter("foo",
                             r'.+foo=(\d+).+',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             regex_flags=re.DOTALL,
                             value=12)
        # Assert something good with dotall update()
        self.assertTrue(pdv)
        pdv.update("\n\nfoo=1212\n\n")
        self.assertEqual(pdv.get_value(), 1212)

        # negative test with no regex_flags
        pdv = RegexParameter("foo",
                             r'.+foo=(\d+).+',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             value=12)
        # Assert something good with dotall update()
        self.assertTrue(pdv)
        pdv.update("\n\nfoo=1212\n\n")
        self.assertEqual(pdv.get_value(), 12)

        self.assertRaises(TypeError,
                          RegexParameter,
                          "foo",
                          r'.*foo=(\d+).*',
                          lambda match: int(match.group(1)),
                          lambda x: str(x),
                          regex_flags="bad flag",
                          value=12)

    def test_format_current(self):
        self.param_dict.add("test_format",
                            r'.*foo=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: x + 5,
                            value=10)
        self.assertEqual(self.param_dict.format("test_format", 20), 25)
        self.assertEqual(self.param_dict.format("test_format"), 15)
        self.assertRaises(KeyError, self.param_dict.format, "bad_name")

    def _assert_metadata_change(self):
        new_dict = self.param_dict.generate_dict()
        log.debug("Generated dictionary: %s", new_dict)
        self.assertEqual(new_dict["qut"][ParameterDictKey.DESCRIPTION],
                         "QutFileDesc")
        self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME],
                         "QutDisplay")
        self.assertEqual(
            new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.UNITS],
            "QutFileUnits")
        self.assertEqual(
            new_dict["qut"][ParameterDictKey.VALUE][
                ParameterDictKey.DESCRIPTION], "QutFileValueDesc")
        self.assertEqual(
            new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.TYPE],
            "QutFileType")
        # Should come from hard code
        # self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutFileName")

        # from base hard code
        new_dict = self.param_dict.generate_dict()
        self.assertEqual(new_dict["baz"][ParameterDictKey.DESCRIPTION],
                         "The baz parameter")
        self.assertEqual(
            new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.UNITS],
            "nano-bazers")
        self.assertEqual(
            new_dict["baz"][ParameterDictKey.VALUE][
                ParameterDictKey.DESCRIPTION],
            "Should be an integer between 2 and 2000")
        self.assertEqual(
            new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.TYPE],
            ParameterDictType.INT)
        self.assertEqual(new_dict["baz"][ParameterDictKey.DISPLAY_NAME], "Baz")

        self.assertTrue('extra_param' not in new_dict)
예제 #5
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)
class TestUnitProtocolParameterDict(TestUnitStringsDict):
    """
    Test cases for instrument driver class. Functions in this class provide
    instrument driver unit tests and provide a tutorial on use of
    the driver interface.
    """

    __test__ = True

    @staticmethod
    def pick_byte2(input_val):
        """ Get the 2nd byte as an example of something tricky and
        arbitrary"""
        val = int(input_val) >> 8
        val &= 255
        return val

    def setUp(self):
        self.param_dict = ProtocolParameterDict()

        self.param_dict.add("foo", r'.*foo=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=True,
                            startup_param=True,
                            default_value=10,
                            visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("bar", r'.*bar=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=False,
                            startup_param=True,
                            default_value=15,
                            visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("baz", r'.*baz=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=True,
                            default_value=20,
                            visibility=ParameterDictVisibility.DIRECT_ACCESS,
                            get_timeout=30,
                            set_timeout=40,
                            display_name="Baz",
                            description="The baz parameter",
                            type=ParameterDictType.INT,
                            units="nano-bazers",
                            value_description="Should be an integer between 2 and 2000")
        self.param_dict.add("bat", r'.*bat=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            default_value=20,
                            visibility=ParameterDictVisibility.READ_ONLY,
                            get_timeout=10,
                            set_timeout=20,
                            display_name="Bat",
                            description="The bat parameter",
                            type=ParameterDictType.INT,
                            units="nano-batbit",
                            value_description="Should be an integer between 1 and 1000")
        self.param_dict.add("qux", r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.READ_ONLY)
        self.param_dict.add("pho", r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.IMMUTABLE)
        self.param_dict.add("dil", r'.*qux=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            startup_param=False,
                            visibility=ParameterDictVisibility.IMMUTABLE)
        self.param_dict.add("qut", r'.*qut=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=True,
                            default_value=[10, 100],
                            visibility=ParameterDictVisibility.DIRECT_ACCESS,
                            expiration=1,
                            get_timeout=10,
                            set_timeout=20,
                            display_name="Qut",
                            description="The qut list parameter",
                            type=ParameterDictType.LIST,
                            units="nano-qutters",
                            value_description="Should be a 2-10 element list of integers between 2 and 2000")

        self.target_schema = {
            "bar": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": True,
                "value": {
                    "default": 15
                },
                "visibility": "READ_WRITE",
                "range": None,
            },
            "bat": {
                "description": "The bat parameter",
                "direct_access": False,
                "display_name": "Bat",
                "get_timeout": 10,
                "set_timeout": 20,
                "startup": False,
                "value": {
                    "default": 20,
                    "description": "Should be an integer between 1 and 1000",
                    "type": "int",
                    "units": "nano-batbit"
                },
                "visibility": "READ_ONLY",
                "range": None,
            },
            "baz": {
                "description": "The baz parameter",
                "direct_access": True,
                "display_name": "Baz",
                "get_timeout": 30,
                "set_timeout": 40,
                "startup": False,
                "value": {
                    "default": 20,
                    "description": "Should be an integer between 2 and 2000",
                    "type": "int",
                    "units": "nano-bazers"
                },
                "visibility": "DIRECT_ACCESS",
                "range": None,
            },
            "dil": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "IMMUTABLE",
                "range": None,
            },
            "foo": {
                "direct_access": True,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": True,
                "value": {
                    "default": 10
                },
                "visibility": "READ_WRITE",
                "range": None,
            },
            "pho": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "IMMUTABLE",
                "range": None,
            },
            "qut": {
                "description": "The qut list parameter",
                "direct_access": True,
                "display_name": "Qut",
                "get_timeout": 10,
                "set_timeout": 20,
                "startup": False,
                "value": {
                    "default": [
                        10,
                        100
                    ],
                    "description": "Should be a 2-10 element list of integers between 2 and 2000",
                    "type": "list",
                    "units": "nano-qutters"
                },
                "visibility": "DIRECT_ACCESS",
                "range": None,
            },
            "qux": {
                "direct_access": False,
                "get_timeout": 10,
                "set_timeout": 10,
                "startup": False,
                "value": {},
                "visibility": "READ_ONLY",
                "range": None,
            }
        }

        self.test_yaml = '''
        parameters: {
            qut: {
            description: "QutFileDesc",
            units: "QutFileUnits",
            value_description: "QutFileValueDesc",
            type: "QutFileType",
            display_name: "QutDisplay"
            },
            extra_param: {
            description: "ExtraFileDesc",
            units: "ExtraFileUnits",
            value_description: "ExtraFileValueDesc",
            type: "ExtraFileType"
            }
          }

        commands: {
          dummy: stuff
          }
        '''

    def test_get_direct_access_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_direct_access_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 3)
        self.assert_("foo" in result)
        self.assert_("baz" in result)
        self.assert_("qut" in result)

    def test_get_startup_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_startup_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("bar" in result)

    def test_set_default(self):
        """
        Test setting a default value
        """
        result = self.param_dict.get_config()
        self.assertEquals(result["foo"], None)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        self.param_dict.update("foo=1000")
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)

        self.assertRaises(ValueError, self.param_dict.set_default, "qux")

    def test_update_many(self):
        """
        Test updating of multiple variables from the same input
        """
        sample_input = """
foo=100
bar=200, baz=300
"""
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertNotEquals(self.param_dict.get("baz"), 300)
        result = self.param_dict.update_many(sample_input)
        log.debug("result: %s", result)
        self.assertEquals(result["foo"], True)
        self.assertEquals(result["bar"], True)
        self.assertEquals(result["baz"], True)
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)
        self.assertEquals(self.param_dict.get("baz"), 300)

    def test_update_specific_values(self):
        """
        test to verify we can limit update to a specific
        set of parameters
        """
        sample_input = "foo=100, bar=200"

        # First verify we can set both
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertTrue(self.param_dict.update(sample_input))
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Now let's only have it update 1 parameter with a name
        sample_input = "foo=200, bar=300"
        self.assertTrue(self.param_dict.update(sample_input, target_params="foo"))
        self.assertEquals(self.param_dict.get("foo"), 200)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Now let's only have it update 1 parameter using a list
        sample_input = "foo=300, bar=400"
        self.assertTrue(self.param_dict.update(sample_input, target_params=["foo"]))
        self.assertEquals(self.param_dict.get("foo"), 300)
        self.assertEquals(self.param_dict.get("bar"), 200)

        # Test our exceptions
        with self.assertRaises(KeyError):
            self.param_dict.update(sample_input, "key_does_not_exist")

        with self.assertRaises(InstrumentParameterException):
            self.param_dict.update(sample_input, {'bad': "key_does_not_exist"})

    def test_visibility_list(self):
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_WRITE)
        lst.sort()
        self.assertEquals(lst, ["bar", "foo"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.DIRECT_ACCESS)
        lst.sort()
        self.assertEquals(lst, ["baz", "qut"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY)
        lst.sort()
        self.assertEquals(lst, ["bat", "qux"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.IMMUTABLE)
        lst.sort()
        self.assertEquals(lst, ["dil", "pho"])

    def test_function_values(self):
        """
        Make sure we can add and update values with functions instead of patterns
        """

        self.param_dict.add_parameter(
                FunctionParameter("fn_foo",
                                  self.pick_byte2,
                                  lambda x: str(x),
                                  direct_access=True,
                                  startup_param=True,
                                  value=1,
                                  visibility=ParameterDictVisibility.READ_WRITE)
        )
        self.param_dict.add_parameter(
                FunctionParameter("fn_bar",
                                  lambda x: bool(x & 2),  # bit map example
                                  lambda x: str(x),
                                  direct_access=True,
                                  startup_param=True,
                                  value=False,
                                  visibility=ParameterDictVisibility.READ_WRITE)
        )

        # check defaults just to be safe
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 1)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        self.param_dict.update(1005)  # just change first in list
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 3)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        # fn_bar does not get updated here
        result = self.param_dict.update_many(1205)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(len(result), 1)
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 4)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)

        # both are updated now
        result = self.param_dict.update_many(6)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(result['fn_bar'], True)
        self.assertEqual(len(result), 2)

        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 0)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, True)

    def test_mixed_pdv_types(self):
        """ Verify we can add different types of PDVs in one container """
        self.param_dict.add_parameter(
                FunctionParameter("fn_foo",
                                  self.pick_byte2,
                                  lambda x: str(x),
                                  direct_access=True,
                                  startup_param=True,
                                  value=1,
                                  visibility=ParameterDictVisibility.READ_WRITE)
        )
        self.param_dict.add_parameter(
                RegexParameter("foo", r'.*foo=(\d+).*',
                               lambda match: int(match.group(1)),
                               lambda x: str(x),
                               direct_access=True,
                               startup_param=True,
                               value=10,
                               visibility=ParameterDictVisibility.READ_WRITE)
        )
        self.param_dict.add("bar", r'.*bar=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: str(x),
                            direct_access=False,
                            startup_param=True,
                            value=15,
                            visibility=ParameterDictVisibility.READ_WRITE)

        self.assertEqual(self.param_dict.get("fn_foo"), 1)
        self.assertEqual(self.param_dict.get("foo"), 10)
        self.assertEqual(self.param_dict.get("bar"), 15)

    def test_base_update(self):
        pdv = Parameter("foo",
                        lambda x: str(x),
                        value=12)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update(1)
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), 1)

        # Its a base class...monkey see, monkey do
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), "foo=1")

    def test_regex_val(self):
        pdv = RegexParameter("foo",
                             r'.*foo=(\d+).*',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             value=12)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update(1)
        self.assertEqual(result, False)
        self.assertEqual(pdv.get_value(), 12)
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.get_value(), 1)

    def test_function_val(self):
        pdv = FunctionParameter("foo",
                                self.pick_byte2,
                                lambda x: str(x),
                                value=12)
        self.assertEqual(pdv.get_value(), 12)
        self.assertRaises(TypeError, pdv.update(1))
        result = pdv.update("1205")
        self.assertEqual(pdv.get_value(), 4)
        self.assertEqual(result, True)

    def test_set_init_value(self):
        result = self.param_dict.get("foo")
        self.assertEqual(result, None)
        self.param_dict.set_init_value("foo", 42)
        result = self.param_dict.get_init_value("foo")
        self.assertEqual(result, 42)

    def test_schema_generation(self):
        self.maxDiff = None
        result = self.param_dict.generate_dict()
        json_result = json.dumps(result, indent=4, sort_keys=True)
        log.debug("Expected: %s", self.target_schema)
        log.debug("Result: %s", json_result)
        self.assertEqual(result, self.target_schema)

    def test_empty_schema(self):
        self.param_dict = ProtocolParameterDict()
        result = self.param_dict.generate_dict()
        self.assertEqual(result, {})

    def test_bad_descriptions(self):
        self.param_dict._param_dict["foo"].description = None
        self.param_dict._param_dict["foo"].value = None
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_init_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_default_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.set_default, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_init_value, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_menu_path_read, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_submenu_read, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_menu_path_write, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_submenu_write, "foo")
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.format, "foo", 1)
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.get_direct_access_list)
        self.assertRaises(InstrumentParameterException,
                          self.param_dict.is_startup_param, "foo")

    def test_set(self):
        """
        Test a simple set of the parameter. Make sure the right values get
        called and the correct exceptions are raised.
        """
        new_param = FunctionParameter("foo",
                                      self.pick_byte2,
                                      lambda x: str(x),
                                      direct_access=True,
                                      startup_param=True,
                                      value=1000,
                                      visibility=ParameterDictVisibility.READ_WRITE)
        self.assertEquals(new_param.get_value(), 1000)
        self.assertEquals(self.param_dict.get("foo"), None)
        # overwrites existing param
        self.param_dict.add_parameter(new_param)
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_value("foo", 2000)
        self.assertEquals(self.param_dict.get("foo"), 2000)

    def test_invalid_type(self):
        self.assertRaises(InstrumentParameterException,
                          FunctionParameter,
                          "fn_bar",
                          lambda x: bool(x & 2),  # bit map example
                          lambda x: str(x),
                          direct_access=True,
                          startup_param=True,
                          value=False,
                          type="bad_type",
                          visibility=ParameterDictVisibility.READ_WRITE)

    def test_get(self):
        """
        test getting values with expiration
        """
        # from mi.core.exceptions import InstrumentParameterExpirationException
        pd = ProtocolParameterDict()

        # No expiration, should work just fine
        pd.add('noexp', r'', None, None, expiration=None)
        pd.add('zeroexp', r'', None, None, expiration=0)
        pd.add('lateexp', r'', None, None, expiration=2)

        ###
        # Set and get with no expire
        ###
        pd.set_value('noexp', 1)
        self.assertEqual(pd.get('noexp'), 1)

        ###
        # Set and get with a 0 expire
        ###
        basetime = pd.get_current_timestamp()
        pd.set_value('zeroexp', 2)

        # We should fail because we are calculating exp against current time
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('zeroexp')

        # Should succeed because exp is calculated using basetime
        self.assertEqual(pd.get('zeroexp', basetime), 2)

        ###
        # Set and get with a delayed expire
        ###
        basetime = pd.get_current_timestamp()
        futuretime = pd.get_current_timestamp(3)
        self.assertGreater(futuretime - basetime, 3)

        pd.set_value('lateexp', 2)

        # Success because data is not expired
        self.assertEqual(pd.get('lateexp', basetime), 2)

        # Fail because data is expired (simulated three seconds from now)
        with self.assertRaises(InstrumentParameterExpirationException):
            pd.get('lateexp', futuretime)

    def test_regex_flags(self):
        pdv = RegexParameter("foo",
                             r'.+foo=(\d+).+',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             regex_flags=re.DOTALL,
                             value=12)
        # Assert something good with dotall update()
        self.assertTrue(pdv)
        pdv.update("\n\nfoo=1212\n\n")
        self.assertEqual(pdv.get_value(), 1212)

        # negative test with no regex_flags
        pdv = RegexParameter("foo",
                             r'.+foo=(\d+).+',
                             lambda match: int(match.group(1)),
                             lambda x: str(x),
                             value=12)
        # Assert something good with dotall update()
        self.assertTrue(pdv)
        pdv.update("\n\nfoo=1212\n\n")
        self.assertEqual(pdv.get_value(), 12)

        self.assertRaises(TypeError,
                          RegexParameter,
                          "foo",
                          r'.*foo=(\d+).*',
                          lambda match: int(match.group(1)),
                          lambda x: str(x),
                          regex_flags="bad flag",
                          value=12)

    def test_format_current(self):
        self.param_dict.add("test_format", r'.*foo=(\d+).*',
                            lambda match: int(match.group(1)),
                            lambda x: x + 5,
                            value=10)
        self.assertEqual(self.param_dict.format("test_format", 20), 25)
        self.assertEqual(self.param_dict.format("test_format"), 15)
        self.assertRaises(KeyError,
                          self.param_dict.format, "bad_name")

    def _assert_metadata_change(self):
        new_dict = self.param_dict.generate_dict()
        log.debug("Generated dictionary: %s", new_dict)
        self.assertEqual(new_dict["qut"][ParameterDictKey.DESCRIPTION], "QutFileDesc")
        self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutDisplay")
        self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.UNITS], "QutFileUnits")
        self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.DESCRIPTION], "QutFileValueDesc")
        self.assertEqual(new_dict["qut"][ParameterDictKey.VALUE][ParameterDictKey.TYPE], "QutFileType")
        # Should come from hard code
        # self.assertEqual(new_dict["qut"][ParameterDictKey.DISPLAY_NAME], "QutFileName")

        # from base hard code
        new_dict = self.param_dict.generate_dict()
        self.assertEqual(new_dict["baz"][ParameterDictKey.DESCRIPTION],
                         "The baz parameter")
        self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.UNITS],
                         "nano-bazers")
        self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.DESCRIPTION],
                         "Should be an integer between 2 and 2000")
        self.assertEqual(new_dict["baz"][ParameterDictKey.VALUE][ParameterDictKey.TYPE],
                         ParameterDictType.INT)
        self.assertEqual(new_dict["baz"][ParameterDictKey.DISPLAY_NAME], "Baz")

        self.assertTrue('extra_param' not in new_dict)
class DataSetDriver(object):
    """
    Base class for data set drivers.  Provides:
    - an interface via callback to publish data
    - an interface via callback to persist driver state
    - an interface via callback to handle exceptions
    - an start and stop sampling
    - a client interface for execute resource

    Subclasses need to include harvesters and parsers and
    be specialized to handle the interaction between the two.
    
    Configurations should contain keys from the DataSetDriverConfigKey class
    and should look something like this example (more full documentation in the
    "Dataset Agent Architecture" page on the OOI wiki):
    {
        'harvester':
        {
            'directory': '/tmp/dsatest',
            'pattern': '*.txt',
            'frequency': 1,
        },
        'parser': {}
        'driver': {
            'records_per_second'
            'harvester_polling_interval'
            'batched_particle_count'
        }
    }
    """
    def __init__(self, config, memento, data_callback, state_callback, exception_callback):
        self._config = config
        self._data_callback = data_callback
        self._state_callback = state_callback
        self._exception_callback = exception_callback
        self._memento = memento
        self._publisher_thread = None

        self._verify_config()

        # Updated my set_resource, defaults defined in build_param_dict
        self._polling_interval = None
        self._generate_particle_count = None
        self._particle_count_per_second = None

        self._param_dict = ProtocolParameterDict()
        self._cmd_dict = ProtocolCommandDict()
        self._driver_dict = DriverDict()

        self._build_command_dict()
        self._build_driver_dict()
        self._build_param_dict()

    def shutdown(self):
        self.stop_sampling()

    def start_sampling(self):
        """
        Start a new thread to monitor for data
        """
        self._start_sampling()
        self._start_publisher_thread()

    def stop_sampling(self):
        """
        Stop the sampling thread
        """
        log.debug("Stopping driver now")

        self._stop_sampling()
        self._stop_publisher_thread()

    def _start_sampling(self):
        raise NotImplementedException('virtual method needs to be specialized')

    def _stop_sampling(self):
        raise NotImplementedException('virtual method needs to be specialized')

    def _is_sampling(self):
        """
        Currently the drivers only have two states, command and streaming and
        all resource commands are common, either start or stop autosample.
        Therefore we didn't implement an enitre state machine to manage states
        and commands.  If it does get more complex than this we should take the
        time to implement a state machine to add some flexibility
        """
        raise NotImplementedException('virtual method needs to be specialized')

    def cmd_dvr(self, cmd, *args, **kwargs):
        log.warn("DRIVER: cmd_dvr %s", cmd)

        if cmd == 'execute_resource':
            resource_cmd = args[0]

            if resource_cmd == DriverEvent.START_AUTOSAMPLE:
                return (ResourceAgentState.STREAMING, None)

            elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE:
                self.stop_sampling()
                return (ResourceAgentState.COMMAND, None)

            else:
                log.error("Unhandled resource command: %s", resource_cmd)
                raise

        elif cmd == 'get_resource_capabilities':
            return self.get_resource_capabilities()

        elif cmd == 'set_resource':
            return self.set_resource(*args, **kwargs)

        elif cmd == 'get_resource':
            return self.get_resource(*args, **kwargs)

        elif cmd == 'get_config_metadata':
            return self.get_config_metadata(*args, **kwargs)

        elif cmd == 'disconnect':
            pass

        elif cmd == 'initialize':
            pass

        else:
            log.error("Unhandled command: %s", cmd)
            raise InstrumentStateException("Unhandled command: %s" % cmd)

    def get_resource_capabilities(self, current_state=True, *args, **kwargs):
        """
        Return driver commands and parameters.
        @param current_state True to retrieve commands available in current
        state, otherwise reutrn all commands.
        @retval list of AgentCapability objects representing the drivers
        capabilities.
        @raises NotImplementedException if not implemented by subclass.
        """
        res_params = self._param_dict.get_keys()
        res_cmds = [DriverEvent.STOP_AUTOSAMPLE, DriverEvent.START_AUTOSAMPLE]

        if current_state and self._is_sampling():
            res_cmds = [DriverEvent.STOP_AUTOSAMPLE]
        elif current_state and not self._is_sampling():
            res_cmds = [DriverEvent.START_AUTOSAMPLE]

        return [res_cmds, res_params]

    def set_resource(self, *args, **kwargs):
        """
        Set the driver parameter
        """
        log.trace("start set_resource")
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        log.trace("set_resource: iterate through params: %s", params)
        for (key, val) in params.iteritems():
            if key in [DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND]:
                if not isinstance(val, int): raise InstrumentParameterException("%s must be an integer" % key)
            if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]:
                if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key)

            if val <= 0:
                raise InstrumentParameterException("%s must be > 0" % key)

            self._param_dict.set_value(key, val)

        # Set the driver parameters
        self._generate_particle_count = self._param_dict.get(DriverParameter.BATCHED_PARTICLE_COUNT)
        self._particle_count_per_second = self._param_dict.get(DriverParameter.RECORDS_PER_SECOND)
        self._polling_interval = self._param_dict.get(DriverParameter.PUBLISHER_POLLING_INTERVAL)
        log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second, self._generate_particle_count)

    def get_resource(self, *args, **kwargs):
        """
        Get driver parameter
        """
        result = {}

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

        # If all params requested, retrieve config.
        if params == DriverParameter.ALL:
            result = self._param_dict.get_config()

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

                except KeyError:
                    raise InstrumentParameterException(('%s is not a valid parameter.' % key))

        return result

    def get_config_metadata(self):
        """
        Return the configuration metadata object in JSON format
        @retval The description of the parameters, commands, and driver info
        in a JSON string
        @see https://confluence.oceanobservatories.org/display/syseng/CIAD+MI+SV+Instrument+Driver-Agent+parameter+and+command+metadata+exchange
        """
        log.debug("Getting metadata from driver...")
        log.debug("Getting metadata dict from protocol...")
        return_dict = {}
        return_dict[ConfigMetadataKey.DRIVER] = self._driver_dict.generate_dict()
        return_dict[ConfigMetadataKey.COMMANDS] = self._cmd_dict.generate_dict()
        return_dict[ConfigMetadataKey.PARAMETERS] = self._param_dict.generate_dict()

        return return_dict

    def _verify_config(self):
        """
        virtual method to verify the supplied driver configuration is value.  Must
        be overloaded in sub classes.

        raises an ConfigurationException when a configuration error is detected.
        """
        raise NotImplementedException('virtual methond needs to be specialized')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        pass

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """
        self._cmd_dict.add(DriverEvent.START_AUTOSAMPLE, display_name="start autosample")
        self._cmd_dict.add(DriverEvent.STOP_AUTOSAMPLE, display_name="stop autosample")

    def _build_param_dict(self):
        """
        Setup three common driver parameters
        """
        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.RECORDS_PER_SECOND,
                int,
                value=60,
                type=ParameterDictType.INT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Records Per Second",
                description="Number of records to process per second")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.PUBLISHER_POLLING_INTERVAL,
                float,
                value=1,
                type=ParameterDictType.FLOAT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Harvester Polling Interval",
                description="Duration in minutes to wait before checking for new files.")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.BATCHED_PARTICLE_COUNT,
                int,
                value=1,
                type=ParameterDictType.INT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Batched Particle Count",
                description="Number of particles to batch before sending to the agent")
        )

        config = self._config.get(DataSourceConfigKey.DRIVER, {})
        log.debug("set_resource on startup with: %s", config)
        self.set_resource(config)

    def _start_publisher_thread(self):
        self._publisher_thread = gevent.spawn(self._publisher_loop)
        self._publisher_shutdown = False

    def _stop_publisher_thread(self):
        log.debug("Signal shutdown")
        self._publisher_shutdown = True
        if self._publisher_thread:
            self._publisher_thread.kill(block=False)
        log.debug("shutdown complete")

    def _publisher_loop(self):
        """
        Main loop to listen for new files to parse.  Parse them and move on.
        """
        log.info("Starting main publishing loop")

        try:
            while(not self._publisher_shutdown):
                self._poll()
                gevent.sleep(self._polling_interval)
        except Exception as e:
            log.error("Exception in publisher thread: %s", e)
            self._exception_callback(e)

        log.debug("publisher thread detected shutdown request")

    def _poll(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def _new_file_exception(self):
        raise NotImplementedException('virtual methond needs to be specialized')
class DataSetDriver(object):
    """
    Base class for data set drivers.  Provides:
    - an interface via callback to publish data
    - an interface via callback to persist driver state
    - an interface via callback to handle exceptions
    - an start and stop sampling
    - a client interface for execute resource

    Subclasses need to include harvesters and parsers and
    be specialized to handle the interaction between the two.
    
    Configurations should contain keys from the DataSetDriverConfigKey class
    and should look something like this example (more full documentation in the
    "Dataset Agent Architecture" page on the OOI wiki):
    {
        'harvester':
        {
            'directory': '/tmp/dsatest',
            'pattern': '*.txt',
            'frequency': 1,
        },
        'parser': {}
        'driver': {
            'records_per_second'
            'harvester_polling_interval'
            'batched_particle_count'
        }
    }
    """
    def __init__(self, config, memento, data_callback, state_callback,
                 exception_callback):
        self._config = config
        self._data_callback = data_callback
        self._state_callback = state_callback
        self._exception_callback = exception_callback
        self._memento = memento
        self._publisher_thread = None

        self._verify_config()
        self._param_dict = ProtocolParameterDict()

        # Updated my set_resource, defaults defined in build_param_dict
        self._polling_interval = None
        self._generate_particle_count = None
        self._particle_count_per_second = None

        self._build_param_dict()

    def shutdown(self):
        self.stop_sampling()

    def start_sampling(self):
        """
        Start a new thread to monitor for data
        """
        self._start_sampling()
        self._start_publisher_thread()

    def stop_sampling(self):
        """
        Stop the sampling thread
        """
        log.debug("Stopping driver now")

        self._stop_sampling()
        self._stop_publisher_thread()

    def _start_sampling(self):
        raise NotImplementedException(
            'virtual methond needs to be specialized')

    def _stop_sampling(self):
        raise NotImplementedException(
            'virtual methond needs to be specialized')

    def cmd_dvr(self, cmd, *args, **kwargs):
        log.warn("DRIVER: cmd_dvr %s", cmd)

        if not cmd in [
                'execute_resource', 'get_resource_capabilities',
                'set_resource', 'get_resource'
        ]:
            log.error("Unhandled command: %s", cmd)
            raise InstrumentStateException("Unhandled command: %s" % cmd)

        resource_cmd = args[0]

        if cmd == 'execute_resource':
            if resource_cmd == DriverEvent.START_AUTOSAMPLE:
                return (ResourceAgentState.STREAMING, None)

            elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE:
                self.stop_sampling()
                return (ResourceAgentState.COMMAND, None)

            else:
                log.error("Unhandled resource command: %s", resource_cmd)
                raise

        elif cmd == 'get_resource_capabilities':
            return self.get_resource_capabilities()

        elif cmd == 'set_resource':
            return self.set_resource(*args, **kwargs)

        elif cmd == 'get_resource':
            return self.get_resource(*args, **kwargs)

    def get_resource_capabilities(self, current_state=True, *args, **kwargs):
        """
        Return driver commands and parameters.
        @param current_state True to retrieve commands available in current
        state, otherwise reutrn all commands.
        @retval list of AgentCapability objects representing the drivers
        capabilities.
        @raises NotImplementedException if not implemented by subclass.
        """
        res_params = self._param_dict.get_keys()
        return [[], res_params]

    def set_resource(self, *args, **kwargs):
        """
        Set the driver parameter
        """
        log.trace("start set_resource")
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException(
                'Set command requires a parameter dict.')

        log.trace("set_resource: iterate through params: %s", params)
        for (key, val) in params.iteritems():
            if key in [
                    DriverParameter.BATCHED_PARTICLE_COUNT,
                    DriverParameter.RECORDS_PER_SECOND
            ]:
                if not isinstance(val, int):
                    raise InstrumentParameterException(
                        "%s must be an integer" % key)
            if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]:
                if not isinstance(val, (int, float)):
                    raise InstrumentParameterException("%s must be an float" %
                                                       key)

            if val <= 0:
                raise InstrumentParameterException("%s must be > 0" % key)

            self._param_dict.set_value(key, val)

        # Set the driver parameters
        self._generate_particle_count = self._param_dict.get(
            DriverParameter.BATCHED_PARTICLE_COUNT)
        self._particle_count_per_second = self._param_dict.get(
            DriverParameter.RECORDS_PER_SECOND)
        self._polling_interval = self._param_dict.get(
            DriverParameter.PUBLISHER_POLLING_INTERVAL)
        log.trace("Driver Parameters: %s, %s, %s", self._polling_interval,
                  self._particle_count_per_second,
                  self._generate_particle_count)

    def get_resource(self, *args, **kwargs):
        """
        Get driver parameter
        """
        result = {}

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

        # If all params requested, retrieve config.
        if params == DriverParameter.ALL:
            result = self._param_dict.get_config()

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

                except KeyError:
                    raise InstrumentParameterException(
                        ('%s is not a valid parameter.' % key))

        return result

    def _verify_config(self):
        """
        virtual method to verify the supplied driver configuration is value.  Must
        be overloaded in sub classes.

        raises an ConfigurationException when a configuration error is detected.
        """
        raise NotImplementedException(
            'virtual methond needs to be specialized')

    def _build_param_dict(self):
        """
        Setup three common driver parameters
        """
        self._param_dict.add_parameter(
            Parameter(DriverParameter.RECORDS_PER_SECOND,
                      int,
                      value=60,
                      type=ParameterDictType.INT,
                      display_name="Records Per Second",
                      description="Number of records to process per second"))

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.PUBLISHER_POLLING_INTERVAL,
                float,
                value=1,
                type=ParameterDictType.FLOAT,
                display_name="Harvester Polling Interval",
                description=
                "Duration in minutes to wait before checking for new files."))

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.BATCHED_PARTICLE_COUNT,
                int,
                value=1,
                type=ParameterDictType.INT,
                display_name="Batched Particle Count",
                description=
                "Number of particles to batch before sending to the agent"))

        config = self._config.get(DataSourceConfigKey.DRIVER, {})
        log.debug("set_resource on startup with: %s", config)
        self.set_resource(config)

    def _start_publisher_thread(self):
        self._publisher_thread = gevent.spawn(self._publisher_loop)
        self._publisher_shutdown = False

    def _stop_publisher_thread(self):
        log.debug("Signal shutdown")
        self._publisher_shutdown = True
        if self._publisher_thread:
            self._publisher_thread.kill(block=False)
        log.debug("shutdown complete")

    def _publisher_loop(self):
        """
        Main loop to listen for new files to parse.  Parse them and move on.
        """
        log.info("Starting main publishing loop")

        try:
            while (not self._publisher_shutdown):
                self._poll()
                gevent.sleep(self._polling_interval)
        except Exception as e:
            log.error("Exception in publisher thread: %s", e)
            self._exception_callback(e)

        log.debug("publisher thread detected shutdown request")

    def _poll(self):
        raise NotImplementedException(
            'virtual methond needs to be specialized')

    def _new_file_exception(self):
        raise NotImplementedException(
            'virtual methond needs to be specialized')
예제 #9
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):
        """
예제 #10
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):
        """
예제 #11
0
class DataSetDriver(object):
    """
    Base class for data set drivers.  Provides:
    - an interface via callback to publish data
    - an interface via callback to persist driver state
    - an interface via callback to handle exceptions
    - an start and stop sampling
    - a client interface for execute resource

    Subclasses need to include harvesters and parsers and
    be specialized to handle the interaction between the two.
    
    Configurations should contain keys from the DataSetDriverConfigKey class
    and should look something like this example (more full documentation in the
    "Dataset Agent Architecture" page on the OOI wiki):
    {
        'harvester':
        {
            'directory': '/tmp/dsatest',
            'storage_directory': '/tmp/stored_dsatest',
            'pattern': '*.txt',
            'frequency': 1,
            'file_mod_wait_time': 30,
        },
        'parser': {}
        'driver': {
            'records_per_second'
            'harvester_polling_interval'
            'batched_particle_count'
        }
    }
    """
    def __init__(self, config, memento, data_callback, state_callback, event_callback, exception_callback):
        self._config = copy.deepcopy(config)
        self._data_callback = data_callback
        self._state_callback = state_callback
        self._event_callback = event_callback
        self._exception_callback = exception_callback
        self._memento = memento
        self._publisher_thread = None

        self._verify_config()

        # Updated my set_resource, defaults defined in build_param_dict
        self._polling_interval = None
        self._generate_particle_count = None
        self._particle_count_per_second = None
        self._resource_id = None

        self._param_dict = ProtocolParameterDict()
        self._cmd_dict = ProtocolCommandDict()
        self._driver_dict = DriverDict()

        self._build_command_dict()
        self._build_driver_dict()
        self._build_param_dict()

    def shutdown(self):
        self.stop_sampling()

    def start_sampling(self):
        """
        Start a new thread to monitor for data
        """
        self._start_sampling()
        self._start_publisher_thread()

    def stop_sampling(self):
        """
        Stop the sampling thread
        """
        log.debug("Stopping sampling and publisher now")

        self._stop_sampling()
        self._stop_publisher_thread()

    def _start_sampling(self):
        raise NotImplementedException('virtual method needs to be specialized')

    def _stop_sampling(self):
        raise NotImplementedException('virtual method needs to be specialized')

    def _is_sampling(self):
        """
        Currently the drivers only have two states, command and streaming and
        all resource commands are common, either start or stop autosample.
        Therefore we didn't implement an enitre state machine to manage states
        and commands.  If it does get more complex than this we should take the
        time to implement a state machine to add some flexibility
        """
        raise NotImplementedException('virtual method needs to be specialized')

    def cmd_dvr(self, cmd, *args, **kwargs):
        log.warn("DRIVER: cmd_dvr %s", cmd)

        if cmd == 'execute_resource':
            resource_cmd = args[0]

            if resource_cmd == DriverEvent.START_AUTOSAMPLE:
                return (ResourceAgentState.STREAMING, None)

            elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE:
                self.stop_sampling()
                return (ResourceAgentState.COMMAND, None)

            else:
                log.error("Unhandled resource command: %s", resource_cmd)
                raise

        elif cmd == 'get_resource_capabilities':
            return self.get_resource_capabilities()

        elif cmd == 'set_resource':
            return self.set_resource(*args, **kwargs)

        elif cmd == 'get_resource':
            return self.get_resource(*args, **kwargs)

        elif cmd == 'get_config_metadata':
            return self.get_config_metadata(*args, **kwargs)

        elif cmd == 'disconnect':
            pass

        elif cmd == 'initialize':
            pass

        else:
            log.error("Unhandled command: %s", cmd)
            raise InstrumentStateException("Unhandled command: %s" % cmd)

    def get_resource_capabilities(self, current_state=True, *args, **kwargs):
        """
        Return driver commands and parameters.
        @param current_state True to retrieve commands available in current
        state, otherwise reutrn all commands.
        @retval list of AgentCapability objects representing the drivers
        capabilities.
        @raises NotImplementedException if not implemented by subclass.
        """
        res_params = self._param_dict.get_keys()
        res_cmds = [DriverEvent.STOP_AUTOSAMPLE, DriverEvent.START_AUTOSAMPLE]

        if current_state and self._is_sampling():
            res_cmds = [DriverEvent.STOP_AUTOSAMPLE]
        elif current_state and not self._is_sampling():
            res_cmds = [DriverEvent.START_AUTOSAMPLE]

        return [res_cmds, res_params]

    def set_resource(self, *args, **kwargs):
        """
        Set the driver parameter
        """
        log.trace("start set_resource")
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        log.trace("set_resource: iterate through params: %s", params)
        for (key, val) in params.iteritems():
            if key in [DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND]:
                if not isinstance(val, int): raise InstrumentParameterException("%s must be an integer" % key)
            if key in [DriverParameter.PUBLISHER_POLLING_INTERVAL]:
                if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key)

            if val <= 0:
                raise InstrumentParameterException("%s must be > 0" % key)

            self._param_dict.set_value(key, val)

        # Set the driver parameters
        self._generate_particle_count = self._param_dict.get(DriverParameter.BATCHED_PARTICLE_COUNT)
        self._particle_count_per_second = self._param_dict.get(DriverParameter.RECORDS_PER_SECOND)
        self._polling_interval = self._param_dict.get(DriverParameter.PUBLISHER_POLLING_INTERVAL)
        log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second,
                  self._generate_particle_count)


    def get_resource(self, *args, **kwargs):
        """
        Get driver parameter
        """
        result = {}

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

        # If all params requested, retrieve config.
        if params == DriverParameter.ALL:
            result = self._param_dict.get_config()

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

                except KeyError:
                    raise InstrumentParameterException(('%s is not a valid parameter.' % key))

        return result

    def get_config_metadata(self):
        """
        Return the configuration metadata object in JSON format
        @retval The description of the parameters, commands, and driver info
        in a JSON string
        @see https://confluence.oceanobservatories.org/display/syseng/CIAD+MI+SV+Instrument+Driver-Agent+parameter+and+command+metadata+exchange
        """
        log.debug("Getting metadata from driver...")
        log.debug("Getting metadata dict from protocol...")
        return_dict = {}
        return_dict[ConfigMetadataKey.DRIVER] = self._driver_dict.generate_dict()
        return_dict[ConfigMetadataKey.COMMANDS] = self._cmd_dict.generate_dict()
        return_dict[ConfigMetadataKey.PARAMETERS] = self._param_dict.generate_dict()

        return return_dict

    def _verify_config(self):
        """
        virtual method to verify the supplied driver configuration is value.  Must
        be overloaded in sub classes.

        raises an ConfigurationException when a configuration error is detected.
        """
        raise NotImplementedException('virtual methond needs to be specialized')

    def _build_driver_dict(self):
        """
        Populate the driver dictionary with options
        """
        pass

    def _build_command_dict(self):
        """
        Populate the command dictionary with command.
        """
        self._cmd_dict.add(DriverEvent.START_AUTOSAMPLE, display_name="start autosample")
        self._cmd_dict.add(DriverEvent.STOP_AUTOSAMPLE, display_name="stop autosample")

    def _build_param_dict(self):
        """
        Setup three common driver parameters
        """
        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.RECORDS_PER_SECOND,
                int,
                value=60,
                type=ParameterDictType.INT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Records Per Second",
                description="Number of records to process per second")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.PUBLISHER_POLLING_INTERVAL,
                float,
                value=1,
                type=ParameterDictType.FLOAT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Harvester Polling Interval",
                description="Duration in minutes to wait before checking for new files.")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.BATCHED_PARTICLE_COUNT,
                int,
                value=1,
                type=ParameterDictType.INT,
                visibility=ParameterDictVisibility.IMMUTABLE,
                display_name="Batched Particle Count",
                description="Number of particles to batch before sending to the agent")
        )

        config = self._config.get(DataSourceConfigKey.DRIVER, {})
        log.debug("set_resource on startup with: %s", config)
        self.set_resource(config)

    def _start_publisher_thread(self):
        self._publisher_thread = gevent.spawn(self._publisher_loop)
        self._publisher_shutdown = False

    def _stop_publisher_thread(self):
        log.debug("Signal shutdown")
        self._publisher_shutdown = True
        if self._publisher_thread:
            self._publisher_thread.kill(block=False)
        log.debug("shutdown complete")

    def _publisher_loop(self):
        """
        Main loop to listen for new files to parse.  Parse them and move on.
        """
        log.info("Starting main publishing loop")

        try:
            while(not self._publisher_shutdown):
                self._poll()
                gevent.sleep(self._polling_interval)
        except Exception as e:
            log.error("Exception in publisher thread (resource id: %s): %s", self._resource_id, traceback.format_exc(e))
            self._exception_callback(e)

        log.debug("publisher thread detected shutdown request")

    def _poll(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def _new_file_exception(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def _sample_exception_callback(self, exception):
        """
        Publish an event when a sample exception is detected
        """
        self._event_callback(event_type="ResourceAgentErrorEvent", error_msg = "%s" % exception)


    def _raise_new_file_event(self, name):
        """
        Raise a ResourceAgentIOEvent when a new file is detected.  Add file stats
        to the payload of the event.
        """
        s = os.stat(name)
        checksum = ""
        with open(name, 'rb') as filehandle:
            checksum = hashlib.md5(filehandle.read()).hexdigest()

        stats = {
            'name': name,
            'size': s.st_size,
            'mod': s.st_mtime,
            'md5_checksum': checksum
        }

        self._event_callback(event_type="ResourceAgentIOEvent", source_type="new file", stats=stats)
class TestUnitProtocolParameterDict(MiUnitTestCase):
    @staticmethod
    def pick_byte2(input):
        """ Get the 2nd byte as an example of something tricky and
        arbitrary"""
        val = int(input) >> 8
        val = val & 255
        return val
    
    """
    Test cases for instrument driver class. Functions in this class provide
    instrument driver unit tests and provide a tutorial on use of
    the driver interface.
    """ 
    def setUp(self):
        self.param_dict = ProtocolParameterDict()
                
        self.param_dict.add("foo", r'.*foo=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=True,
                             startup_param=True,
                             default_value=10,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("bar", r'.*bar=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=False,
                             startup_param=True,
                             default_value=15,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("baz", r'.*baz=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=True,
                             default_value=20,
                             visibility=ParameterDictVisibility.DIRECT_ACCESS)
        self.param_dict.add("bat", r'.*bat=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             startup_param=False,
                             default_value=20,
                             visibility=ParameterDictVisibility.READ_ONLY)
        self.param_dict.add("qux", r'.*qux=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             startup_param=False,
                             visibility=ParameterDictVisibility.READ_ONLY)
        
    def test_get_direct_access_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_direct_access_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("baz" in result)
        
    def test_get_startup_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_startup_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("bar" in result)
        
    def test_set_default(self):
        """
        Test setting a default value
        """
        result = self.param_dict.get_config()
        self.assertEquals(result["foo"], None)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        self.param_dict.update("foo=1000")
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        
        self.assertRaises(ValueError, self.param_dict.set_default, "qux")
        
    def test_update_many(self):
        """
        Test updating of multiple variables from the same input
        """
        sample_input = """
foo=100
bar=200, baz=300
"""
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertNotEquals(self.param_dict.get("baz"), 300)
        result = self.param_dict.update_many(sample_input)
        log.debug("result: %s", result)
        self.assertEquals(result["foo"], True)
        self.assertEquals(result["bar"], True)
        self.assertEquals(result["baz"], True)
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)
        self.assertEquals(self.param_dict.get("baz"), 300)
        
    def test_visibility_list(self):
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_WRITE)
        self.assertEquals(lst, ["foo", "bar"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.DIRECT_ACCESS)
        self.assertEquals(lst, ["baz"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY)
        self.assertEquals(lst, ["bat", "qux"])
        
    def test_function_values(self):
        """
        Make sure we can add and update values with functions instead of patterns
        """

        self.param_dict.add_paramdictval(
            FunctionParamDictVal(
                "fn_foo",
                self.pick_byte2,
                lambda x : str(x),
                direct_access=True,
                startup_param=True,
                value=1,
                visibility=ParameterDictVisibility.READ_WRITE)
            )
        self.param_dict.add_paramdictval(
            FunctionParamDictVal(
                "fn_bar",
                lambda x : bool(x&2), # bit map example
                lambda x : str(x),
                direct_access=True,
                startup_param=True,
                value=False,
                visibility=ParameterDictVisibility.READ_WRITE)
            )
        
        # check defaults just to be safe
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 1)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)
        
        result = self.param_dict.update(1005) # just change first in list
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 3)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)
        
        # fn_bar does not get updated here
        result = self.param_dict.update_many(1205)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(len(result), 1)
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 4)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, False)
        
        # both are updated now
        result = self.param_dict.update_many(6)
        self.assertEqual(result['fn_foo'], True)
        self.assertEqual(result['fn_bar'], True)
        self.assertEqual(len(result), 2)
        
        val = self.param_dict.get("fn_foo")
        self.assertEqual(val, 0)
        val = self.param_dict.get("fn_bar")
        self.assertEqual(val, True)
        
    def test_mixed_pdv_types(self):
        """ Verify we can add different types of PDVs in one container """
        self.param_dict.add_paramdictval(
            FunctionParamDictVal(
                "fn_foo",
                self.pick_byte2,
                lambda x : str(x),
                direct_access=True,
                startup_param=True,
                value=1,
                visibility=ParameterDictVisibility.READ_WRITE)
            )
        self.param_dict.add_paramdictval(
            RegexParamDictVal("foo", r'.*foo=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=True,
                             startup_param=True,
                             value=10,
                             visibility=ParameterDictVisibility.READ_WRITE)
            )
        self.param_dict.add("bar", r'.*bar=(\d+).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=False,
                             startup_param=True,
                             value=15,
                             visibility=ParameterDictVisibility.READ_WRITE)
        
        self.assertEqual(self.param_dict.get("fn_foo"), 1)
        self.assertEqual(self.param_dict.get("foo"), 10)
        self.assertEqual(self.param_dict.get("bar"), 15)
        
    def test_base_update(self):
        pdv = ParameterDictVal("foo",
                               lambda x : str(x),
                               value=12)
        self.assertEqual(pdv.value, 12)
        result = pdv.update(1)
        self.assertEqual(result, True)
        self.assertEqual(pdv.value, 1)

        # Its a base class...monkey see, monkey do
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.value, "foo=1")
        
    def test_regex_val(self):
        pdv = RegexParamDictVal("foo",
                               r'.*foo=(\d+).*',
                               lambda match : int(match.group(1)),
                               lambda x : str(x),
                               value=12)
        self.assertEqual(pdv.value, 12)
        result = pdv.update(1)
        self.assertEqual(result, False)
        self.assertEqual(pdv.value, 12)
        result = pdv.update("foo=1")
        self.assertEqual(result, True)
        self.assertEqual(pdv.value, 1)
        
    def test_function_val(self):
        pdv = FunctionParamDictVal("foo",
                               self.pick_byte2,
                               lambda x : str(x),
                               value=12)
        self.assertEqual(pdv.value, 12)
        self.assertRaises(TypeError, pdv.update(1))
        result = pdv.update("1205")
        self.assertEqual(pdv.value, 4)
        self.assertEqual(result, True)
        
    def test_set_init_value(self):
        result = self.param_dict.get("foo")
        self.assertEqual(result, None)        
        self.param_dict.set_init_value("foo", 42)
        result = self.param_dict.get_init_value("foo")
        self.assertEqual(result, 42)
class InstrumentProtocol(object):
    """
        
    Base instrument protocol class.
    """    
    def __init__(self, driver_event):
        """
        Base constructor.
        @param driver_event The callback for asynchronous driver events.
        """
        # Event callback to send asynchronous events to the agent.
        self._driver_event = driver_event

        # The connection used to talk to the device.
        self._connection = None
        
        # The protocol state machine.
        self._protocol_fsm = None
        
        # The parameter dictionary.
        self._param_dict = ProtocolParameterDict()

        # The spot to stash a configuration before going into direct access
        # mode
        self._pre_direct_access_config = None

        # Driver configuration passed from the user
        self._startup_config = {}

        # scheduler config is a bit redundant now, but if we ever want to
        # re-initialize a scheduler we will need it.
        self._scheduler = None
        self._scheduler_callback = {}
        self._scheduler_config = {}

    ########################################################################
    # Helper methods
    ########################################################################
    def got_data(self, port_agent_packet):
        """
        Called by the instrument connection when data is available.
         Defined in subclasses.
        """
        log.error("base got_data.  Who called me?")
        pass

    def _extract_sample(self, particle_class, regex, line, publish=True):
        """
        Extract sample from a response line if present and publish
        parsed particle

        @param particle_class The class to instantiate for this specific
            data particle. Parameterizing this allows for simple, standard
            behavior from this routine
        @param regex The regular expression that matches a data sample
        @param line string to match for sample.
        @param publish boolean to publish samples (default True). If True,
               two different events are published: one to notify raw data and
               the other to notify parsed data.

        @retval dict of dicts {'parsed': parsed_sample, 'raw': raw_sample} if
                the line can be parsed for a sample. Otherwise, None.
        @todo Figure out how the agent wants the results for a single poll
            and return them that way from here
        """

        sample = None
        if regex.match(line):
        
            particle = particle_class(line,
                preferred_timestamp=DataParticleKey.DRIVER_TIMESTAMP)

            parsed_sample = particle.generate()

            if publish and self._driver_event:
                self._driver_event(DriverAsyncEvent.SAMPLE, parsed_sample)
    
            sample = json.loads(parsed_sample)
            return sample
        return sample

    def get_current_state(self):
        """
        Return current state of the protocol FSM.
        """

        return self._protocol_fsm.get_current_state()

    def get_resource_capabilities(self, current_state=True):
        """
        """

        res_cmds = self._protocol_fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)        
        res_params = self._param_dict.get_keys()
        
        return [res_cmds, res_params]

    def _filter_capabilities(self, events):
        """
        """

        return events

    ########################################################################
    # Scheduler interface.
    ########################################################################
    def _add_scheduler(self, name, callback):
        """
        Stage a scheduler in a driver.  The job will actually be configured
        and started by initialize_scheduler

        @param name the name of the job
        @param callback the handler when the job is triggered
        @raise KeyError if we try to add a job twice
        """
        if(self._scheduler_callback.get(name)):
            raise KeyError("duplicate scheduler exists for '%s'" % name)

        self._scheduler_callback[name] = callback
        self._add_scheduler_job(name)

    def _add_scheduler_job(self, name):
        """
        Map the driver configuration to a scheduler configuration.  If
        the scheduler has been started then also add the job.
        @param name the name of the job
        @raise KeyError if job name does not exists in the callback config
        @raise KeyError if job is already configured
        """
        # Do nothing if the scheduler isn't initialized
        if(not self._scheduler):
            return

        callback = self._scheduler_callback.get(name)
        if(not callback):
            raise KeyError("callback not defined in driver for '%s'" % name)

        if(self._scheduler_config.get(name)):
            raise KeyError("scheduler job already configured '%s'" % name)

        scheduler_config = self._get_scheduler_config()

        # No config?  Nothing to do then.
        if(scheduler_config == None):
            return

        job_config = scheduler_config.get(name)

        if(job_config):
            # Store the scheduler configuration
            self._scheduler_config[name] = {
                DriverSchedulerConfigKey.TRIGGER: job_config.get(DriverSchedulerConfigKey.TRIGGER),
                DriverSchedulerConfigKey.CALLBACK: callback
            }
            config = {name: self._scheduler_config[name]}
            log.debug("Scheduler job with config: %s" % config)

            # start the job.  Note, this lazily starts the scheduler too :)
            self._scheduler.add_config(config)

    def _get_scheduler_config(self):
        """
        Get the configuration dictionary to use for initializing jobs

        Returned dictionary structure:
        {
            'job_name': {
                DriverSchedulerConfigKey.TRIGGER: {}
            }
        }

        @return: scheduler configuration dictionary
        """
        # Currently the startup config is in the child class.
        # @TODO should the config code be promoted?
        config = self._startup_config
        return config.get(DriverConfigKey.SCHEDULER)

    def initialize_scheduler(self):
        """
        Activate all configured schedulers added using _add_scheduler.
        Timers start when the job is activated.
        """
        if(self._scheduler == None):
            self._scheduler = DriverScheduler()
            for name in self._scheduler_callback.keys():
                self._add_scheduler_job(name)

    #############################################################
    # Configuration logic
    #############################################################
    
    def set_init_params(self, config):
        """
        Set the initialization parameters to the given values in the protocol
        parameter dictionary. 
        @param config The parameter_name/value to set in the initialization
            fields of the parameter dictionary
        @raise InstrumentParameterException If the config cannot be set
        """
        if not isinstance(config, dict):
            raise InstrumentParameterException("Invalid init config format")

        self._startup_config = config

        param_config = config.get(DriverConfigKey.PARAMETERS)
        if(param_config):
            for name in param_config.keys():
                self._param_dict.set_init_value(name, param_config[name])
    
    def get_startup_config(self):
        """
        Gets the startup configuration for the instrument. The parameters
        returned are marked as startup, and the values are the best as chosen
        from the initialization, default, and current parameters.
        
        @retval The dict of parameter_name/values (override this method if it
            is more involved for a specific instrument) that should be set at
            a higher level.
        """
        return_dict = {}
        start_list = self._param_dict.get_startup_list()
        log.trace("Startup list: %s", start_list)
        assert isinstance(start_list, list)
        
        for param in start_list:
            result = self._param_dict.get_init_value(param)
            if result != None:
                log.trace("Got init value for %s: %s", param, result)
                return_dict[param] = result
            else:
                result = self._param_dict.get_default_value(param)
                if result != None:
                    log.trace("Got default value for %s: %s", param, result)
                    return_dict[param] = result
                else:
                    log.trace("Got current value for %s: %s", param, result)
                    return_dict[param] = self._param_dict.get(param)
        
        return return_dict
        
    def get_direct_access_params(self):
        """
        Get the list of direct access parameters, useful for restoring direct
        access configurations up in the driver.
        
        @retval a list of direct access parameter names
        """
        return self._param_dict.get_direct_access_list()
        
    def get_cached_config(self):
        """
        Return the configuration object that shows the instrument's 
        configuration as cached in the parameter dictionary...usually in
        sync with the instrument, but accessible when offline...
        @retval The cached configuration in the instruments config format. By
        default, it is a dictionary of parameter names and values.
        """
        assert self._param_dict != None
        return self._param_dict.get_config()
        
    ########################################################################
    # Command build and response parse handlers.
    ########################################################################            
    def _add_response_handler(self, cmd, func, state=None):
        """
        Insert a handler class responsible for handling the response to a
        command sent to the instrument, optionally available only in a
        specific state.
        
        @param cmd The high level key of the command to respond to.
        @param func The function that handles the response
        @param state The state to pair with the command for which the function
        should be used
        """

        if state == None:
            self._response_handlers[cmd] = func
        else:            
            self._response_handlers[(state, cmd)] = func

    def _add_build_handler(self, cmd, func):
        """
        Add a command building function.
        @param cmd The device command to build.
        @param func The function that constructs the command.
        """

        self._build_handlers[cmd] = func
        
    ########################################################################
    # Helpers to build commands.
    ########################################################################
    def _build_simple_command(self, cmd, *args):
        """
        Builder for simple commands

        @param cmd The command to build
        @param args Unused arguments
        @retval Returns string ready for sending to instrument        
        """

        return "%s%s" % (cmd, self._newline)
    
    def _build_keypress_command(self, cmd, *args):
        """
        Builder for simple, non-EOLN-terminated commands

        @param cmd The command to build
        @param args Unused arguments
        @retval Returns string ready for sending to instrument        
        """


        return "%s" % (cmd)
    
    def _build_multi_keypress_command(self, cmd, *args):
        """
        Builder for simple, non-EOLN-terminated commands

        @param cmd The command to build
        @param args Unused arguments
        @retval Returns string ready for sending to instrument        
        """


        return "%s%s%s%s%s%s" % (cmd, cmd, cmd, cmd, cmd, cmd)

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

    def _true_false_to_string(v):
        """
        Write a boolean value to string formatted for "generic" set operations.
        Subclasses should overload this as needed for instrument-specific
        formatting.
        
        @param v a boolean value.
        @retval A yes/no string formatted as a Python boolean for set operations.
        @throws InstrumentParameterException if value not a bool.
        """
        
        if not isinstance(v,bool):
            raise InstrumentParameterException('Value %s is not a bool.' % str(v))
        return str(v)

    @staticmethod
    def _int_to_string(v):
        """
        Write an int value to string formatted for "generic" set operations.
        Subclasses should overload this as needed for instrument-specific
        formatting.
        
        @param v An int val.
        @retval an int string formatted for generic set operations.
        @throws InstrumentParameterException if value not an int.
        """
        
        if not isinstance(v,int):
            raise InstrumentParameterException('Value %s is not an int.' % str(v))
        else:
            return '%i' % v

    @staticmethod
    def _float_to_string(v):
        """
        Write a float value to string formatted for "generic" set operations.
        Subclasses should overload this as needed for instrument-specific
        formatting.
        
        @param v A float val.
        @retval a float string formatted for "generic" set operations.
        @throws InstrumentParameterException if value is not a float.
        """

        if not isinstance(v,float):
            raise InstrumentParameterException('Value %s is not a float.' % v)
        else:
            return '%e' % v
class TestUnitProtocolParameterDict(MiUnitTestCase):
    """
    Test cases for instrument driver class. Functions in this class provide
    instrument driver unit tests and provide a tutorial on use of
    the driver interface.
    """ 
    def setUp(self):
        self.param_dict = ProtocolParameterDict()
                
        self.param_dict.add("foo", r'.*foo=(\d*).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=True,
                             startup_param=True,
                             default_value=10,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("bar", r'.*bar=(\d*).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=False,
                             startup_param=True,
                             default_value=15,
                             visibility=ParameterDictVisibility.READ_WRITE)
        self.param_dict.add("baz", r'.*baz=(\d*).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             direct_access=True,
                             default_value=20,
                             visibility=ParameterDictVisibility.DIRECT_ACCESS)
        self.param_dict.add("bat", r'.*bat=(\d*).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             startup_param=False,
                             default_value=20,
                             visibility=ParameterDictVisibility.READ_ONLY)
        self.param_dict.add("qux", r'.*qux=(\d*).*',
                             lambda match : int(match.group(1)),
                             lambda x : str(x),
                             startup_param=False,
                             visibility=ParameterDictVisibility.READ_ONLY)
        
    def test_get_direct_access_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_direct_access_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("baz" in result)
        
    def test_get_startup_list(self):
        """
        Test to see we can get a list of direct access parameters
        """
        result = self.param_dict.get_startup_list()
        self.assertTrue(isinstance(result, list))
        self.assertEquals(len(result), 2)
        self.assert_("foo" in result)
        self.assert_("bar" in result)
        
    def test_set_default(self):
        """
        Test setting a default value
        """
        result = self.param_dict.get_config()
        self.assertEquals(result["foo"], None)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        self.param_dict.update("foo=1000")
        self.assertEquals(self.param_dict.get("foo"), 1000)
        self.param_dict.set_default("foo")
        self.assertEquals(self.param_dict.get("foo"), 10)
        
        self.assertRaises(ValueError, self.param_dict.set_default, "qux")
        
    def test_update_many(self):
        """
        Test updating of multiple variables from the same input
        """
        sample_input = """
foo=100
bar=200, baz=300
"""
        self.assertNotEquals(self.param_dict.get("foo"), 100)
        self.assertNotEquals(self.param_dict.get("bar"), 200)
        self.assertNotEquals(self.param_dict.get("baz"), 300)
        result = self.param_dict.update_many(sample_input)
        log.debug("result: %s", result)
        self.assertEquals(result["foo"], True)
        self.assertEquals(result["bar"], True)
        self.assertEquals(result["baz"], True)
        self.assertEquals(self.param_dict.get("foo"), 100)
        self.assertEquals(self.param_dict.get("bar"), 200)
        self.assertEquals(self.param_dict.get("baz"), 300)
        
    def test_visibility_list(self):
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_WRITE)
        self.assertEquals(lst, ["foo", "bar"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.DIRECT_ACCESS)
        self.assertEquals(lst, ["baz"])
        lst = self.param_dict.get_visibility_list(ParameterDictVisibility.READ_ONLY)
        self.assertEquals(lst, ["bat", "qux"])
예제 #15
0
class DataSetDriver(object):
    """
    Base class for data set drivers.  Provides:
    - an interface via callback to publish data
    - an interface via callback to persist driver state
    - an interface via callback to handle exceptions
    - an start and stop sampling
    - a client interface for execute resource

    Subclasses need to include harvesters and parsers and
    be specialized to handle the interaction between the two.
    
    Configurations should contain keys from the DataSetDriverConfigKey class
    and should look something like this example (more full documentation in the
    "Dataset Agent Architecture" page on the OOI wiki):
    {
        'harvester':
        {
            'directory': '/tmp/dsatest',
            'pattern': '*.txt',
            'frequency': 1,
        },
        'parser': {}
        'driver': {
            'records_per_second'
            'harvester_polling_interval'
            'batched_particle_count'
        }
    }
    """
    def __init__(self, config, memento, data_callback, state_callback, exception_callback):
        self._config = config
        self._data_callback = data_callback
        self._state_callback = state_callback
        self._exception_callback = exception_callback
        self._memento = memento

        self._verify_config()
        self._param_dict = ProtocolParameterDict()

        # Updated my set_resource, defaults defined in build_param_dict
        self._polling_interval = None
        self._generate_particle_count = None
        self._particle_count_per_second = None

        self._build_param_dict()

    def start_sampling(self):
        """
        Start a new thread to monitor for data
        """
        self._start_sampling()
        self._start_publisher_thread()

    def stop_sampling(self):
        """
        Stop the sampling thread
        """
        self._stop_publisher_thread()

    def _start_sampling(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def _stop_sampling(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def cmd_dvr(self, cmd, *args, **kwargs):
        log.warn("DRIVER: cmd_dvr %s", cmd)

        if not cmd in ['execute_resource', 'get_resource_capabilities', 'set_resource', 'get_resource']:
            log.error("Unhandled command: %s", cmd)
            raise InstrumentStateException("Unhandled command: %s" % cmd)

        resource_cmd = args[0]

        if cmd == 'execute_resource':
            if resource_cmd == DriverEvent.START_AUTOSAMPLE:
                try:
                    log.debug("start autosample")
                    self.start_sampling()
                except:
                    log.error("Failed to start sampling", exc_info=True)
                    raise

                return (ResourceAgentState.STREAMING, None)

            elif resource_cmd == DriverEvent.STOP_AUTOSAMPLE:
                log.debug("stop autosample")
                self.stop_sampling()
                return (ResourceAgentState.COMMAND, None)

            else:
                log.error("Unhandled resource command: %s", resource_cmd)
                raise

        elif cmd == 'get_resource_capabilities':
            return self.get_resource_capabilities()

        elif cmd == 'set_resource':
            return self.set_resource(*args, **kwargs)

        elif cmd == 'get_resource':
            return self.get_resource(*args, **kwargs)

    def get_resource_capabilities(self, current_state=True, *args, **kwargs):
        """
        Return driver commands and parameters.
        @param current_state True to retrieve commands available in current
        state, otherwise reutrn all commands.
        @retval list of AgentCapability objects representing the drivers
        capabilities.
        @raises NotImplementedException if not implemented by subclass.
        """
        res_params = self._param_dict.get_keys()
        return [[], res_params]

    def set_resource(self, *args, **kwargs):
        """
        Set the driver parameter
        """
        log.trace("start set_resource")
        try:
            params = args[0]
        except IndexError:
            raise InstrumentParameterException('Set command requires a parameter dict.')

        log.trace("set_resource: iterate through params: %s", params)
        for (key, val) in params.iteritems():
            if key in [DriverParameter.BATCHED_PARTICLE_COUNT, DriverParameter.RECORDS_PER_SECOND]:
                if not isinstance(val, int): raise InstrumentParameterException("%s must be an integer" % key)
            if key in [DriverParameter.HARVESTER_POLLING_INTERVAL]:
                if not isinstance(val, (int, float)): raise InstrumentParameterException("%s must be an float" % key)

            if val <= 0:
                raise InstrumentParameterException("%s must be > 0" % key)

            self._param_dict.set_value(key, val)

        # Set the driver parameters
        self._generate_particle_count = self._param_dict.get(DriverParameter.BATCHED_PARTICLE_COUNT)
        self._particle_count_per_second = self._param_dict.get(DriverParameter.RECORDS_PER_SECOND)
        self._polling_interval = self._param_dict.get(DriverParameter.HARVESTER_POLLING_INTERVAL)
        log.trace("Driver Parameters: %s, %s, %s", self._polling_interval, self._particle_count_per_second, self._generate_particle_count)

    def get_resource(self, *args, **kwargs):
        """
        Get driver parameter
        """
        result = {}

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

        # If all params requested, retrieve config.
        if params == DriverParameter.ALL:
            result = self._param_dict.get_config()

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

                except KeyError:
                    raise InstrumentParameterException(('%s is not a valid parameter.' % key))

        return result

    def _verify_config(self):
        """
        virtual method to verify the supplied driver configuration is value.  Must
        be overloaded in sub classes.

        raises an ConfigurationException when a configuration error is detected.
        """
        raise NotImplementedException('virtual methond needs to be specialized')

    def _build_param_dict(self):
        """
        Setup three common driver parameters
        """
        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.RECORDS_PER_SECOND,
                int,
                value=60,
                type=ParameterDictType.INT,
                display_name="Records Per Second",
                description="Number of records to process per second")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.HARVESTER_POLLING_INTERVAL,
                float,
                value=1,
                type=ParameterDictType.FLOAT,
                display_name="Harvester Polling Interval",
                description="Duration in minutes to wait before checking for new files.")
        )

        self._param_dict.add_parameter(
            Parameter(
                DriverParameter.BATCHED_PARTICLE_COUNT,
                int,
                value=1,
                type=ParameterDictType.INT,
                display_name="Batched Particle Count",
                description="Number of particles to batch before sending to the agent")
        )

        config = self._config.get(DataSourceConfigKey.DRIVER, {})
        log.debug("set_resource on startup with: %s", config)
        self.set_resource(config)

    def _start_publisher_thread(self):
        self._publisher_thread = gevent.spawn(self._poll)

    def _stop_publisher_thread(self):
        self._publisher_thread.kill()

    def _poll(self):
        raise NotImplementedException('virtual methond needs to be specialized')

    def _new_file_exception(self):
        raise NotImplementedException('virtual methond needs to be specialized')