Beispiel #1
0
    def _construct_fsm(self, states=PlatformDriverState,
                       events=PlatformDriverEvent,
                       enter_event=PlatformDriverEvent.ENTER,
                       exit_event=PlatformDriverEvent.EXIT):
        """
        Constructs the FSM for the driver. The preparations here are mostly
        related with the UNCONFIGURED, DISCONNECTED, and CONNECTED state
        transitions, with some common handlers for the CONNECTED state.
        Subclasses can override to indicate specific parameters and add new
        handlers (typically for the CONNECTED state).
        """
        log.debug("%r: constructing base platform driver FSM", self._platform_id)

        self._fsm = ThreadSafeFSM(states, events, enter_event, exit_event)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, enter_event, self._common_state_enter)
            self._fsm.add_handler(state, exit_event, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_disconnected_disconnect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET, self._handler_connected_get)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET, self._handler_connected_set)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.EXECUTE, self._handler_connected_execute)
Beispiel #2
0
 def setUp(self):
     
     self.fsm = ThreadSafeFSM(MyStates, MyEvents, MyEvents.ENTER, MyEvents.EXIT)
     
     self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_1, self._handler_s1_e1)
     self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_2, self._handler_s1_e2)
     self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_1, self._handler_s2_e1)
     self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_2, self._handler_s2_e2)
     
     self.test_value = None
     
     self.fsm.start(MyStates.STATE_1)
    def _construct_fsm(self):
        """
        """
        log.debug("constructing fsm")

        self._fsm = ThreadSafeFSM(PlatformDriverState,
                                  PlatformDriverEvent,
                                  PlatformDriverEvent.ENTER,
                                  PlatformDriverEvent.EXIT)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, PlatformDriverEvent.ENTER, self._common_state_enter)
            self._fsm.add_handler(state, PlatformDriverEvent.EXIT, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_disconnect)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_METADATA, self._handler_connected_get_metadata)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_ATTRIBUTE_VALUES, self._handler_connected_get_attribute_values)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET_ATTRIBUTE_VALUES, self._handler_connected_set_attribute_values)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECT_INSTRUMENT, self._handler_connected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT_INSTRUMENT, self._handler_disconnected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CONNECTED_INSTRUMENTS, self._handler_connected_get_connected_instruments)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_ON_PORT, self._handler_connected_turn_on_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_OFF_PORT, self._handler_connected_turn_off_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CHECKSUM, self._handler_connected_get_checksum)
Beispiel #4
0
    def setUp(self):

        self.fsm = ThreadSafeFSM(MyStates, MyEvents, MyEvents.ENTER, MyEvents.EXIT)

        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_1, self._handler_s1_e1)
        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_2, self._handler_s1_e2)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_1, self._handler_s2_e1)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_2, self._handler_s2_e2)

        self.test_value = None

        self.fsm.start(MyStates.STATE_1)
Beispiel #5
0
class TestThreadSafeFSM(PyonTestCase):

    def setUp(self):
        
        self.fsm = ThreadSafeFSM(MyStates, MyEvents, MyEvents.ENTER, MyEvents.EXIT)
        
        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_1, self._handler_s1_e1)
        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_2, self._handler_s1_e2)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_1, self._handler_s2_e1)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_2, self._handler_s2_e2)
        
        self.test_value = None
        
        self.fsm.start(MyStates.STATE_1)

    def _handler_s1_e1(self, value, *args, **kwargs):
        start_time = time.time()
        gevent.sleep(0)
        self.test_value = value
        end_time = time.time()
        return (None, (start_time, end_time))
    
    def _handler_s1_e2(self, *args, **kwargs):
        return (None, None)

    def _handler_s2_e1(self, *args, **kwargs):
        return (None, None)

    def _handler_s2_e2(self, *args, **kwargs):
        return (None, None)
    
    def test_state(self):
        
        gl1 = gevent.spawn(self.fsm.on_event, MyEvents.EVENT_1, 100)
        gl2 = gevent.spawn(self.fsm.on_event, MyEvents.EVENT_1, 200)
                
        (g1_start, g1_end) = gl1.get()
        print str(self.test_value)
        self.assertEquals(self.test_value, 100)
        (g2_start, g2_end) = gl2.get()
        print str(self.test_value)
        self.assertEquals(self.test_value, 200)
        self.assertGreater(g2_start, g1_end)
Beispiel #6
0
class TestThreadSafeFSM(PyonTestCase):
    def setUp(self):

        self.fsm = ThreadSafeFSM(MyStates, MyEvents, MyEvents.ENTER, MyEvents.EXIT)

        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_1, self._handler_s1_e1)
        self.fsm.add_handler(MyStates.STATE_1, MyEvents.EVENT_2, self._handler_s1_e2)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_1, self._handler_s2_e1)
        self.fsm.add_handler(MyStates.STATE_2, MyEvents.EVENT_2, self._handler_s2_e2)

        self.test_value = None

        self.fsm.start(MyStates.STATE_1)

    def _handler_s1_e1(self, value, *args, **kwargs):
        start_time = time.time()
        gevent.sleep(0)
        self.test_value = value
        end_time = time.time()
        return (None, (start_time, end_time))

    def _handler_s1_e2(self, *args, **kwargs):
        return (None, None)

    def _handler_s2_e1(self, *args, **kwargs):
        return (None, None)

    def _handler_s2_e2(self, *args, **kwargs):
        return (None, None)

    def test_state(self):

        gl1 = gevent.spawn(self.fsm.on_event, MyEvents.EVENT_1, 100)
        gl2 = gevent.spawn(self.fsm.on_event, MyEvents.EVENT_1, 200)

        (g1_start, g1_end) = gl1.get()
        print str(self.test_value)
        self.assertEquals(self.test_value, 100)
        (g2_start, g2_end) = gl2.get()
        print str(self.test_value)
        self.assertEquals(self.test_value, 200)
        self.assertGreater(g2_start, g1_end)
Beispiel #7
0
class PlatformDriver(object):
    """
    A platform driver handles a particular platform in a platform network.
    This base class provides a common interface and supporting functionality.
    """

    def __init__(self, pnode, event_callback,
                 create_event_subscriber, destroy_event_subscriber):
        """
        Creates a PlatformDriver instance.

        @param pnode           Root PlatformNode defining the platform network
                               rooted at this platform.
        @param event_callback  Callback to notify platform agent about events
                               generated by this driver.
                               This is captured in self._send_event for this
                               class and subclasses to call as needed.

        @param create_event_subscriber
        @param destroy_event_subscriber
                 functions to create/destroy any needed EventSubscriber's,
                 in particular regarding the Managed Endpoint API.
        """

        self._pnode = pnode
        self._send_event = event_callback

        self._create_event_subscriber = create_event_subscriber
        self._destroy_event_subscriber = destroy_event_subscriber

        self._platform_id = self._pnode.platform_id
        if self._pnode.parent:
            self._parent_platform_id = self._pnode.parent.platform_id
        else:
            self._parent_platform_id = None

        self._driver_config = None
        self._resource_schema = {}
        
        # The parameter dictionary.
        self._param_dict = {}

        # construct FSM and start it with initial state UNCONFIGURED:
        self._construct_fsm()
        self._fsm.start(PlatformDriverState.UNCONFIGURED)

    def get_platform_driver_event_class(self):
        """
        Returns PlatformDriverEvent in this base class, but this is typically
        overwritten.
        """
        return PlatformDriverEvent

    def get_platform_driver_capability_class(self):
        """
        Returns PlatformDriverCapability in this base class, but this is typically
        overwritten.
        """
        return PlatformDriverCapability

    def get_resource_capabilities(self, current_state=True, cmd_attrs=False):
        """
        @param current_state
        @param cmd_attrs   If true, the returned commands will be the actual
                           attributes of the associated capability class (or
                           subclass) instead of the associated values.
        """
        res_cmds = self._fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds, cmd_attrs=cmd_attrs)
        res_params = self._param_dict.keys()

        return [res_cmds, res_params]

    def _filter_capabilities(self, events, cmd_attrs=False):
        """
        @param events      the events to filter
        @param cmd_attrs   If true, then the actual attributes of the
                           PlatformDriverCapability class (or subclass) are
                           returned instead of the associated values.
        """
        capability_class = self.get_platform_driver_capability_class()
        event_values = [x for x in events if capability_class.has(x)]

        if not cmd_attrs:
            return event_values

        # map event_values to the actual enum attributes:
        event_attrs = []
        for attr in dir(capability_class):
            # first two checks below similar to BaseEnum.list()
            if attr.startswith('__'):
                continue
            val = getattr(capability_class, attr)
            if callable(val):
                continue

            if val in event_values:
                event_attrs.append(attr)

        return event_attrs

    def get_resource_state(self, *args, **kwargs):
        """
        Return the current state of the driver.
        @retval str current driver state.
        """
        return self._fsm.get_current_state()

    def get_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.GET, *args, **kwargs)

    def set_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.SET, *args, **kwargs)

    def execute_resource(self, resource_cmd, *args, **kwargs):
        """
        Platform agent calls this directly to trigger the execution of a
        resource command. The actual action occurs in execute.
        """
        return self._fsm.on_event(PlatformDriverEvent.EXECUTE, resource_cmd, *args, **kwargs)

    def validate_driver_configuration(self, driver_config):
        """
        Called by configure so a subclass can perform any needed additional
        validation of the provided configuration.
        Nothing is done in this base class. Note that basic validation is
        done by PlatformAgent prior to creating/configuring the driver.

        @param driver_config Driver configuration.

        @raise PlatformDriverException Error in driver configuration.
        """
        pass

    def configure(self, driver_config):
        """
        Configures this driver. In this base class it basically
        calls validate_driver_configuration and then assigns the given
        config to self._driver_config.

        @param driver_config Driver configuration.
        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: configure: %s" % (self._platform_id, str(driver_config)))

        self.validate_driver_configuration(driver_config)
        self._driver_config = driver_config
        #self._param_dict = deepcopy(self._driver_config.get('attributes',{}))
        
    def get_config_metadata(self):
        """
        """
        return deepcopy(self._resource_schema)

    def connect(self, recursion=None):
        """
        To be implemented by subclass.
        Establishes communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect(self, recursion=None):
        """
        To be implemented by subclass.
        Ends communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def ping(self):
        """
        To be implemented by subclass.
        Verifies communication with external platform returning "PONG" if
        this verification completes OK.

        @retval "PONG"

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attributes(self):
        """
        To be implemented by subclass.
        Returns the attributes of this platform. This is used by the agent
        for attribute monitoring purposes.

        @retval {attr_id: dict, ...}
                dict indexed by attribute ID with associated properties.
                attr_id is in particular used during get_attribute_values
                calls to retrieve values during resource monitoring.
                The dict for each attribute should contain the following properties:
                - monitor_cycle_seconds: nominal period in seconds for monitoring

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Returns the values for specific attributes since a given time for
        each attribute.

        @param attrs     [(attr_id, from_time), ...] desired attributes.
                         from_time Assummed to be in the format basically described by
                         pyon's get_ion_ts function, "a str representing an
                         integer number, the millis in UNIX epoch."

        @retval {attr_id : [(attrValue, timestamp), ...], ...}
                dict indexed by attribute name with list of (value, timestamp)
                pairs. Timestamps in same format as from_time.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def supports_set_operation(self):
        """
        @return True only if the SET operation is supported by this driver.
        """
        return False

    def set_attribute_values(self, attrs):
        """
        Sets values for writable attributes in this platform.
        Only called by SET handler when supports_set_operation() returns True.

        @param attrs 	[(attr_id, attrValue), ...] 	List of attribute values

        @retval {attr_id : [(attrValue, timestamp), ...], ...}
                dict with a list of (value,timestamp) pairs for each attribute
                indicated in the input. Returned timestamps indicate the time when the
                value was set. Each timestamp is "a str representing an
                integer number, the millis in UNIX epoch" to
                align with description of pyon's get_ion_ts function.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def execute(self, cmd, *args, **kwargs):
        """
        Executes the given command.
        Subclasses can override to execute particular commands or delegate to
        its super class. However, note that this base class raises
        NotImplementedError.

        @param cmd     command
        @param args    command's args
        @param kwargs  command's kwargs

        @return  result of the execution

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def get(self, *args, **kwargs):
        """
        Gets the values of the requested attributes.
        Subclasses can override to get particular attributes and
        delegate to this base implementation to handle common attributes.

        @param args    get's args
        @param kwargs  get's kwargs

        @return  result of the retrieval.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def destroy(self):
        """
        Stops all activity done by the driver. Nothing done in this class.
        """
        pass

    def get_driver_state(self):
        """
        Returns the current FSM state.
        """
        return self._fsm.get_current_state()

    #####################################################################
    # Supporting method for handling connection lost in CONNECT handlers
    #####################################################################

    def _connection_lost(self, cmd, args, kwargs, exc=None):
        """
        Supporting method to be called by any CONNECTED handler right after
        detecting that the connection with the external platform device has
        been lost. It does a regular disconnect() and notifies the agent about
        the lost connection. Note that the call to disconnect() itself may
        throw some additional exception very likely caused by the fact that
        the connection is lost--this exception is just logged out but ignored.

        All parameters are for logging purposes.

        @param cmd     string indicating the command that was attempted
        @param args    args of the command that was attempted
        @param kwargs  kwargs of the command that was attempted
        @param exc     associated exception (if any),

        @return (next_state, result) suitable as the return of the FSM
                handler where the connection lost was detected. The
                next_state will always be PlatformDriverState.DISCONNECTED.
        """
        log.debug("%r: (LC) _connection_lost: cmd=%s, args=%s, kwargs=%s, exc=%s",
                  self._platform_id, cmd, args, kwargs, exc)

        result = None
        try:
            result = self.disconnect()

        except Exception as e:
            # just log a message
            log.debug("%r: (LC) ignoring exception while calling disconnect upon"
                      " lost connection: %s", self._platform_id, e)

        # in any case, notify the agent about the lost connection and
        # transition to DISCONNECTED:
        self._send_event(AsyncAgentEvent(PlatformAgentEvent.LOST_CONNECTION))

        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    ##############################################################
    # FSM event handlers.
    ##############################################################

    def _common_state_enter(self, *args, **kwargs):
        """
        Common work upon every state entry.
        """
        state = self.get_driver_state()
        log.debug('%r: driver entering state: %s', self._platform_id, state)

        self._send_event(StateChangeDriverEvent(state))

    def _common_state_exit(self, *args, **kwargs):
        """
        Common work upon every state exit.
        Nothing done in this base class.
        """

    ##############################################################
    # UNCONFIGURED event handlers.
    ##############################################################

    def _handler_unconfigured_configure(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        driver_config = kwargs.get('driver_config', None)
        if driver_config is None:
            raise FSMError('configure: missing driver_config argument')

        try:
            result = self.configure(driver_config)
            next_state = PlatformDriverState.DISCONNECTED
        except PlatformDriverException as e:
            result = None
            next_state = None
            log.error("%r: Error in platform driver configuration", self._platform_id, e)

        return next_state, result

    ##############################################################
    # DISCONNECTED event handlers.
    ##############################################################

    def _handler_disconnected_connect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        recursion = kwargs.get('recursion', None)

        self.connect(recursion=recursion)
        result = next_state = PlatformDriverState.CONNECTED

        return next_state, result

    def _handler_disconnected_disconnect(self, *args, **kwargs):
        """
        We allow the DISCONNECT event in DISCONNECTED state for convenience,
        in particular it facilitates the overall handling of the connection_lost
        event, which is processed by a subsequent call to disconnect from the
        platform agent. The handler here does nothing.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        return None, None

    ###########################################################################
    # CONNECTED event handlers.
    # Except for the explicit disconnect and connection_lost handlers, the
    # CONNECTED handlers (here and in subclasses) should directly catch any
    # PlatformConnectionException to call _connection_lost.
    ###########################################################################

    def _handler_connected_disconnect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        recursion = kwargs.get('recursion', None)

        result = self.disconnect(recursion=recursion)
        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    def _handler_connected_connection_lost(self, *args, **kwargs):
        """
        The connection was lost (as opposed to a normal disconnect request).
        Here we do the regular disconnect but also notify the platform agent
        about the lost connection.

        NOTE: this handler in the FSM is provided in case there is a need to
        directly trigger the associated transition along with the associated
        notification to the agent. However, the typical case is that a CONNECTED
        handler dealing with commands will catch any PlatformConnectionException
        to call _connection_lost directly.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        # just use our supporting method:
        return self._connection_lost(PlatformDriverEvent.CONNECTION_LOST, args, kwargs)

    def _handler_connected_ping(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        try:
            result = self.ping()
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.PING, args, kwargs, e)

    def _handler_connected_get(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        try:
            result = self.get(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.GET, args, kwargs, e)

    def _handler_connected_set(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        if not self.supports_set_operation():
            raise FSMError('Unsupported operation: %s' % PlatformDriverEvent.SET)

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('set_attribute_values: missing attrs argument')

        try:
            result = self.set_attribute_values(attrs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.SET, args, kwargs, e)

    def _handler_connected_execute(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        if len(args) == 0:
            raise FSMError('execute_resource: missing resource_cmd argument')

        try:
            result = self.execute(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.EXECUTE, args, kwargs, e)

    ##############################################################
    # Platform driver FSM setup
    ##############################################################

    def _construct_fsm(self, states=PlatformDriverState,
                       events=PlatformDriverEvent,
                       enter_event=PlatformDriverEvent.ENTER,
                       exit_event=PlatformDriverEvent.EXIT):
        """
        Constructs the FSM for the driver. The preparations here are mostly
        related with the UNCONFIGURED, DISCONNECTED, and CONNECTED state
        transitions, with some common handlers for the CONNECTED state.
        Subclasses can override to indicate specific parameters and add new
        handlers (typically for the CONNECTED state).
        """
        log.debug("%r: constructing base platform driver FSM", self._platform_id)

        self._fsm = ThreadSafeFSM(states, events, enter_event, exit_event)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, enter_event, self._common_state_enter)
            self._fsm.add_handler(state, exit_event, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_disconnected_disconnect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET, self._handler_connected_get)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET, self._handler_connected_set)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.EXECUTE, self._handler_connected_execute)
class PlatformDriver(object):
    """
    A platform driver handles a particular platform in a platform network.
    This base class provides a common interface and supporting functionality.
    """

    def __init__(self, pnode, event_callback):
        """
        Creates a PlatformDriver instance.

        @param pnode           Root PlatformNode defining the platform network
                               rooted at this platform.
        @param event_callback  Listener of events generated by this driver
        """

        #
        # NOTE the "pnode" parameter may be not very "standard" but it is the
        # current convenient mechanism that captures the overall definition
        # of the corresponding platform (most of which coming from configuration)
        #

        assert pnode, "pnode must be given"
        assert event_callback, "event_callback parameter must be given"

        self._pnode = pnode
        self._send_event = event_callback

        self._platform_id = self._pnode.platform_id
        if self._pnode.parent:
            self._parent_platform_id = self._pnode.parent.platform_id
        else:
            self._parent_platform_id = None

        self._platform_attributes = \
            dict((a.attr_id, a.defn) for a in self._pnode.attrs.itervalues())

        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: PlatformDriver constructor called: pnode:\n%s\n"
                      "_platform_attributes=%s",
                      self._platform_id,
                      NetworkUtil._dump_pnode(self._pnode, include_subplatforms=False),
                      self._platform_attributes)

        self._driver_config = None
        self._resource_schema = {}
        
        # The parameter dictionary.
        self._param_dict = {}

        # construct FSM and start it with initial state UNCONFIGURED:
        self._construct_fsm()
        self._fsm.start(PlatformDriverState.UNCONFIGURED)

    def get_resource_capabilities(self, current_state=True):
        """
        """
        res_cmds = self._fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)
        res_params = self._param_dict.keys()

        return [res_cmds, res_params]

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

    def get_resource_state(self, *args, **kwargs):
        """
        Return the current state of the driver.
        @retval str current driver state.
        """
        return self._fsm.get_current_state()

    def get_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.GET, *args, **kwargs)

    def set_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.SET, *args, **kwargs)

    def execute_resource(self, resource_cmd, *args, **kwargs):
        """
        Platform agent calls this directly to trigger the execution of a
        resource command. The actual action occurs in execute.
        """
        return self._fsm.on_event(PlatformDriverEvent.EXECUTE, resource_cmd, *args, **kwargs)

    def _get_platform_attributes(self):
        """
        Gets a dict of the attribute definitions in this platform as given at
        construction time (from pnode parameter).
        """
        return self._platform_attributes

    def validate_driver_configuration(self, driver_config):
        """
        Called by configure so a subclass can perform any needed additional
        validation of the provided configuration.
        Nothing is done in this base class. Note that basic validation is
        done by PlatformAgent prior to creating/configuring the driver.

        @param driver_config Driver configuration.

        @raise PlatformDriverException Error in driver configuration.
        """
        pass

    def configure(self, driver_config):
        """
        Configures this driver. In this base class it basically
        calls validate_driver_configuration and then assigns the given
        config to self._driver_config.

        @param driver_config Driver configuration.
        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: configure: %s" % (self._platform_id, str(driver_config)))

        self.validate_driver_configuration(driver_config)
        self._driver_config = driver_config
        #self._param_dict = deepcopy(self._driver_config.get('attributes',{}))
        
    def get_config_metadata(self):
        """
        """
        return deepcopy(self._resource_schema)

    def connect(self):
        """
        To be implemented by subclass.
        Establishes communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect(self):
        """
        To be implemented by subclass.
        Ends communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def ping(self):
        """
        To be implemented by subclass.
        Verifies communication with external platform returning "PONG" if
        this verification completes OK.

        @retval "PONG"

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Returns the values for specific attributes since a given time for
        each attribute.

        @param attrs     [(attrName, from_time), ...] desired attributes.
                         from_time Assummed to be in the format basically described by
                         pyon's get_ion_ts function, "a str representing an
                         integer number, the millis in UNIX epoch."

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict indexed by attribute name with list of (value, timestamp)
                pairs. Timestamps in same format as from_time.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def set_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Sets values for writable attributes in this platform.

        @param attrs 	[(attrName, attrValue), ...] 	List of attribute values

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict with a list of (value,timestamp) pairs for each attribute
                indicated in the input. Returned timestamps indicate the time when the
                value was set. Each timestamp is "a str representing an
                integer number, the millis in UNIX epoch" to
                align with description of pyon's get_ion_ts function.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        #
        # TODO Any needed alignment with the instrument case?
        #
        raise NotImplementedError()  #pragma: no cover

    def execute(self, cmd, *args, **kwargs):
        """
        Executes the given command.
        Subclasses can override to execute particular commands or delegate to
        its super class. However, note that this base class raises
        NotImplementedError.

        @param cmd     command
        @param args    command's args
        @param kwargs  command's kwargs

        @return  result of the execution

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def get(self, *args, **kwargs):
        """
        Gets the values of the requested attributes.
        Subclasses can override to get particular attributes and
        delegate to this base implementation to handle common attributes.

        @param args    get's args
        @param kwargs  get's kwargs

        @return  result of the retrieval.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def destroy(self):
        """
        Stops all activity done by the driver. Nothing done in this class.
        """
        pass

    def _notify_driver_event(self, driver_event):
        """
        Convenience method for subclasses to send a driver event to
        corresponding platform agent.

        @param driver_event a DriverEvent object.
        """
        log.debug("%r: _notify_driver_event: %s", self._platform_id, driver_event)

        assert isinstance(driver_event, DriverEvent)

        self._send_event(driver_event)

    def get_external_checksum(self):
        """
        To be implemented by subclass.
        Returns the checksum of the external platform associated with this
        driver.

        @return SHA1 hash value as string of hexadecimal digits.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_driver_state(self):
        """
        Returns the current FSM state.
        """
        return self._fsm.get_current_state()

    #####################################################################
    # Supporting method for handling connection lost in CONNECT handlers
    #####################################################################

    def _connection_lost(self, cmd, args, kwargs, exc=None):
        """
        Supporting method to be called by any CONNECTED handler right after
        detecting that the connection with the external platform device has
        been lost. It does a regular disconnect() and notifies the agent about
        the lost connection. Note that the call to disconnect() itself may
        throw some additional exception very likely caused by the fact that
        the connection is lost--this exception is just logged out but ignored.

        All parameters are for logging purposes.

        @param cmd     string indicating the command that was attempted
        @param args    args of the command that was attempted
        @param kwargs  kwargs of the command that was attempted
        @param exc     associated exception (if any),

        @return (next_state, result) suitable as the return of the FSM
                handler where the connection lost was detected. The
                next_state will always be PlatformDriverState.DISCONNECTED.
        """
        log.debug("%r: (LC) _connection_lost: cmd=%s, args=%s, kwargs=%s, exc=%s",
                  self._platform_id, cmd, args, kwargs, exc)

        # NOTE I have this import here as a quick way to avoid circular imports
        # (note that platform_agent also imports elements in this module)
        # TODO better to move some basic definitions to separate modules.
        from ion.agents.platform.platform_agent import PlatformAgentEvent

        result = None
        try:
            result = self.disconnect()

        except Exception as e:
            # just log a message
            log.debug("%r: (LC) ignoring exception while calling disconnect upon"
                      " lost connection: %s", self._platform_id, e)

        # in any case, notify the agent about the lost connection and
        # transition to DISCONNECTED:
        self._notify_driver_event(AsyncAgentEvent(PlatformAgentEvent.LOST_CONNECTION))

        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    ##############################################################
    # FSM event handlers.
    ##############################################################

    def _common_state_enter(self, *args, **kwargs):
        """
        Common work upon every state entry.
        """
        state = self.get_driver_state()
        log.debug('%r: driver entering state: %s', self._platform_id, state)

        self._notify_driver_event(StateChangeDriverEvent(state))

    def _common_state_exit(self, *args, **kwargs):
        """
        Common work upon every state exit.
        Nothing done in this base class.
        """

    ##############################################################
    # UNCONFIGURED event handlers.
    ##############################################################

    def _handler_unconfigured_configure(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        driver_config = kwargs.get('driver_config', None)
        if driver_config is None:
            raise FSMError('configure: missing driver_config argument')

        try:
            result = self.configure(driver_config)
            next_state = PlatformDriverState.DISCONNECTED
        except PlatformDriverException as e:
            result = None
            next_state = None
            log.error("Error in platform driver configuration", e)

        return next_state, result

    ##############################################################
    # DISCONNECTED event handlers.
    ##############################################################

    def _handler_disconnected_connect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        self.connect()
        result = next_state = PlatformDriverState.CONNECTED

        return next_state, result

    def _handler_disconnected_disconnect(self, *args, **kwargs):
        """
        We allow the DISCONNECT event in DISCONNECTED state for convenience,
        in particular it facilitates the overall handling of the connection_lost
        event, which is processed by a subsequent call to disconnect from the
        platform agent. The handler here does nothing.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        return None, None

    ###########################################################################
    # CONNECTED event handlers.
    # Except for the explicit disconnect and connection_lost handlers, the
    # CONNECTED handlers (here and in subclasses) should directly catch any
    # PlatformConnectionException to call _connection_lost.
    ###########################################################################

    def _handler_connected_disconnect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.disconnect()
        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    def _handler_connected_connection_lost(self, *args, **kwargs):
        """
        The connection was lost (as opposed to a normal disconnect request).
        Here we do the regular disconnect but also notify the platform agent
        about the lost connection.

        NOTE: this handler in the FSM is provided in case there is a need to
        directly trigger the associated transition along with the associated
        notification to the agent. However, the typical case is that a CONNECTED
        handler dealing with commands will catch any PlatformConnectionException
        to call _connection_lost directly.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        # just use our supporting method:
        return self._connection_lost(PlatformDriverEvent.CONNECTION_LOST, args, kwargs)

    def _handler_connected_ping(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        try:
            result = self.ping()
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.PING, args, kwargs, e)

    def _handler_connected_get(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        try:
            result = self.get(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.GET, args, kwargs, e)

    def _handler_connected_set(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('set_attribute_values: missing attrs argument')

        try:
            result = self.set_attribute_values(attrs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.SET, args, kwargs, e)

    def _handler_connected_execute(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        if len(args) == 0:
            raise FSMError('execute_resource: missing resource_cmd argument')

        try:
            result = self.execute(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.EXECUTE, args, kwargs, e)

    ##############################################################
    # Platform driver FSM setup
    ##############################################################

    def _construct_fsm(self, states=PlatformDriverState,
                       events=PlatformDriverEvent,
                       enter_event=PlatformDriverEvent.ENTER,
                       exit_event=PlatformDriverEvent.EXIT):
        """
        Constructs the FSM for the driver. The preparations here are mostly
        related with the UNCONFIGURED, DISCONNECTED, and CONNECTED state
        transitions, with some common handlers for the CONNECTED state.
        Subclasses can override to indicate specific parameters and add new
        handlers (typically for the CONNECTED state).
        """
        log.debug("constructing base platform driver FSM")

        self._fsm = ThreadSafeFSM(states, events, enter_event, exit_event)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, enter_event, self._common_state_enter)
            self._fsm.add_handler(state, exit_event, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_disconnected_disconnect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_connection_lost)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET, self._handler_connected_get)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET, self._handler_connected_set)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.EXECUTE, self._handler_connected_execute)
class PlatformDriver(object):
    """
    A platform driver handles a particular platform in a platform network.
    """

    def __init__(self, pnode, event_callback):
        """
        Creates a PlatformDriver instance.

        @param pnode           Root PlatformNode defining the platform network
                               rooted at this platform.
        @param event_callback  Listener of events generated by this driver
        """
        assert pnode, "pnode must be given"
        assert event_callback, "event_callback parameter must be given"

        self._pnode = pnode
        self._send_event = event_callback

        self._platform_id = self._pnode.platform_id
        if self._pnode.parent:
            self._parent_platform_id = self._pnode.parent.platform_id
        else:
            self._parent_platform_id = None

        self._platform_attributes = \
            dict((a.attr_id, a.defn) for a in self._pnode.attrs.itervalues())

        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: PlatformDriver constructor called: pnode:\n%s\n"
                      "_platform_attributes=%s",
                      self._platform_id,
                      NetworkUtil._dump_pnode(self._pnode, include_subplatforms=False),
                      self._platform_attributes)

        self._driver_config = None

        # The parameter dictionary.
        self._param_dict = {}

        # construct FSM and start it with initial state UNCONFIGURED:
        self._construct_fsm()
        self._fsm.start(PlatformDriverState.UNCONFIGURED)

    def get_resource_capabilities(self, current_state=True):
        """
        """
        res_cmds = self._fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)
        res_params = self._param_dict.keys()

        return [res_cmds, res_params]

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

    def get_resource_state(self, *args, **kwargs):
        """
        Return the current state of the driver.
        @retval str current driver state.
        """
        return self._fsm.get_current_state()

    def get_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.GET, *args, **kwargs)

    def set_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.SET, *args, **kwargs)

    def execute_resource(self, resource_cmd, *args, **kwargs):
        """
        Platform agent calls this directly to trigger the execution of a
        resource command. The actual action occurs in _execute.
        """
        return self._fsm.on_event(PlatformDriverEvent.EXECUTE, resource_cmd, *args, **kwargs)

    def _get_platform_attributes(self):
        """
        Gets a dict of the attribute definitions in this platform as given at
        construction time (from pnode parameter).
        """
        return self._platform_attributes

    def _validate_driver_configuration(self, driver_config):
        """
        Called by configure so a subclass can perform any needed additional
        validation of the provided configuration.
        Nothing is done in this base class. Note that basic validation is
        done by PlatformAgent prior to creating/configuring the driver.

        @param driver_config Driver configuration.

        @raise PlatformDriverException Error in driver configuration.
        """

    def configure(self, driver_config):
        """
        Configures this driver. It first calls _validate_driver_configuration.

        @param driver_config Driver configuration.
        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: configure: %s" % (self._platform_id, str(driver_config)))

        self._validate_driver_configuration(driver_config)
        self._driver_config = driver_config

    def connect(self):
        """
        To be implemented by subclass.
        Establishes communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect(self):
        """
        To be implemented by subclass.
        Ends communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def ping(self):
        """
        To be implemented by subclass.
        Verifies communication with external platform returning "PONG" if
        this verification completes OK.

        @retval "PONG"
        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_metadata(self):
        """
        To be implemented by subclass.
        Returns the metadata associated to the platform.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Returns the values for specific attributes since a given time for
        each attribute.

        @param attrs     [(attrName, from_time), ...] desired attributes.
                         from_time Assummed to be in the format basically described by
                         pyon's get_ion_ts function, "a str representing an
                         integer number, the millis in UNIX epoch."

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict indexed by attribute name with list of (value, timestamp)
                pairs. Timestamps in same format as from_time.
        """
        raise NotImplementedError()  #pragma: no cover

    def set_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Sets values for writable attributes in this platform.

        @param attrs 	[(attrName, attrValue), ...] 	List of attribute values

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict with a list of (value,timestamp) pairs for each attribute
                indicated in the input. Returned timestamps indicate the time when the
                value was set. Each timestamp is "a str representing an
                integer number, the millis in UNIX epoch" to
                align with description of pyon's get_ion_ts function.

        """
        #
        # TODO Any needed alignment with the instrument case?
        #
        raise NotImplementedError()  #pragma: no cover

    def _execute(self, cmd, *args, **kwargs):
        """
        Executes the given command.
        Subclasses can override to execute particular commands or delegate to
        this base implementation to handle common commands.

        @param cmd   command

        @return
        """

        if cmd == PlatformDriverEvent.GET_CHECKSUM:
            result = self.get_checksum()

        elif cmd == PlatformDriverEvent.GET_PORTS:
            result = self._get_ports()

        elif cmd == PlatformDriverEvent.TURN_ON_PORT:
            result = self.turn_on_port(*args, **kwargs)

        elif cmd == PlatformDriverEvent.TURN_OFF_PORT:
            result = self.turn_off_port(*args, **kwargs)

        elif cmd == PlatformDriverEvent.CONNECT_INSTRUMENT:
            result = self.connect_instrument(*args, **kwargs)

        elif cmd == PlatformDriverEvent.DISCONNECT_INSTRUMENT:
            result = self.disconnect_instrument(*args, **kwargs)

        elif cmd == PlatformDriverEvent.GET_CONNECTED_INSTRUMENTS:
            result = self.get_connected_instruments(*args, **kwargs)

        elif cmd == PlatformDriverEvent.GET_METADATA:
            result = self.get_metadata()

        else:
            # TODO do actual execution
            result = "!! TODO !!: result of _execute: cmd=%s args=%s kwargs=%s" % (
                     cmd, str(args), str(kwargs))

        return result

    def _get_ports(self):
        ports = {}
        for port_id, port in self._pnode.ports.iteritems():
            ports[port_id] = {'network': port.network}
        log.debug("%r: _get_ports: %s", self._platform_id, ports)
        return ports

    def connect_instrument(self, port_id, instrument_id, attributes):
        """
        To be implemented by subclass.
        Connects an instrument to a port in this platform.

        @param port_id      Port ID
        @param instrument_id Instrument ID
        @param attributes   Attribute dictionary

        @retval The resulting configuration for the instrument.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect_instrument(self, port_id, instrument_id):
        """
        To be implemented by subclass.
        Disconnects an instrument from a port in this platform.

        @param port_id      Port ID
        @param instrument_id Instrument ID

        @retval

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_connected_instruments(self, port_id):
        """
        To be implemented by subclass.
        Retrieves the IDs of the instruments connected to a port.

        @param port_id      Port ID

        @retval

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def turn_on_port(self, port_id):
        """
        To be implemented by subclass.
        Turns on a port in this platform.

        @param port_id      Port ID

        @retval The resulting on/off of the port.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def turn_off_port(self, port_id):
        """
        To be implemented by subclass.
        Turns off a port in this platform.

        @param port_id      Port ID

        @retval The resulting on/off of the port.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def destroy(self):
        """
        Stops all activity done by the driver. Nothing done in this class.
        (previously it stopped resource monitoring).
        """

    def _notify_driver_event(self, driver_event):
        """
        Convenience method for subclasses to send a driver event to
        corresponding platform agent.

        @param driver_event a DriverEvent object.
        """
        log.debug("platform driver=%r: notify driver_event=%s",
            self._platform_id, driver_event)

        assert isinstance(driver_event, DriverEvent)

        self._send_event(driver_event)

    def get_checksum(self):
        """
        To be implemented by subclass.
        Returns the checksum for this platform.

        @return SHA1 hash value as string of hexadecimal digits.
        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_driver_state(self):
        """
        Returns the current FSM state.
        """
        return self._fsm.get_current_state()

    ##############################################################
    # FSM event handlers.
    ##############################################################

    def _common_state_enter(self, *args, **kwargs):
        """
        Common work upon every state entry.
        """
        state = self.get_driver_state()
        log.debug('%r: driver entering state: %s', self._platform_id, state)

        self._notify_driver_event(StateChangeDriverEvent(state))

    def _common_state_exit(self, *args, **kwargs):
        """
        Common work upon every state exit.
        Nothing done in this base class.
        """

    ##############################################################
    # UNCONFIGURED event handlers.
    ##############################################################

    def _handler_unconfigured_configure(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        driver_config = kwargs.get('driver_config', None)
        if driver_config is None:
            raise FSMError('configure: missing driver_config argument')

        try:
            result = self.configure(driver_config)
            next_state = PlatformDriverState.DISCONNECTED
        except PlatformDriverException as e:
            result = None
            next_state = None
            log.error("Error in platform driver configuration", e)

        return next_state, result

    ##############################################################
    # DISCONNECTED event handlers.
    ##############################################################

    def _handler_disconnected_connect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.connect()
        next_state = PlatformDriverState.CONNECTED

        return next_state, result

    ##############################################################
    # CONNECTED event handlers.
    ##############################################################

    def _handler_connected_disconnect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.disconnect(*args, **kwargs)
        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    def _handler_connected_ping(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = None
        try:
            result = self.ping()
            next_state = None
        except PlatformConnectionException as e:
            next_state = PlatformDriverState.DISCONNECTED
            log.error("Ping failed", e)

        return next_state, result

    def _handler_connected_get_metadata(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.get_metadata()
        next_state = None

        return next_state, result

    def _handler_connected_get(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('get_attribute_values: missing attrs argument')

        result = self.get_attribute_values(attrs)
        next_state = None

        return next_state, result

    def _handler_connected_set(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('set_attribute_values: missing attrs argument')

        result = self.set_attribute_values(attrs)
        next_state = None

        return next_state, result

    def _handler_connected_execute(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        if len(args) == 0:
            raise FSMError('execute_resource: missing resource_cmd argument')

        result = self._execute(*args, **kwargs)
        next_state = None

        return next_state, result

    def _handler_connected_get_ports(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self._get_ports()
        next_state = None

        return next_state, result

    def _handler_connected_connect_instrument(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('connect_instrument: missing port_id argument')

        instrument_id = kwargs.get('instrument_id', None)
        if instrument_id is None:
            raise FSMError('connect_instrument: missing instrument_id argument')

        attributes = kwargs.get('attributes', None)
        if attributes is None:
            raise FSMError('connect_instrument: missing attributes argument')

        result = self.connect_instrument(port_id, instrument_id, attributes)
        next_state = None

        return next_state, result

    def _handler_disconnected_connect_instrument(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('disconnect_instrument: missing port_id argument')

        instrument_id = kwargs.get('instrument_id', None)
        if instrument_id is None:
            raise FSMError('disconnect_instrument: missing instrument_id argument')

        result = self.disconnect_instrument(port_id, instrument_id)
        next_state = None

        return next_state, result

    def _handler_connected_get_connected_instruments(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('get_connected_instruments: missing port_id argument')

        result = self.get_connected_instruments(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_turn_on_port(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('turn_on_port: missing port_id argument')

        result = self.turn_on_port(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_turn_off_port(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('turn_off_port: missing port_id argument')

        result = self.turn_off_port(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_get_checksum(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.get_checksum()
        next_state = None

        return next_state, result

    ##############################################################
    # Platform driver FSM setup
    ##############################################################

    def _construct_fsm(self):
        """
        """
        log.debug("constructing fsm")

        self._fsm = ThreadSafeFSM(PlatformDriverState,
                                  PlatformDriverEvent,
                                  PlatformDriverEvent.ENTER,
                                  PlatformDriverEvent.EXIT)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, PlatformDriverEvent.ENTER, self._common_state_enter)
            self._fsm.add_handler(state, PlatformDriverEvent.EXIT, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_disconnect)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_METADATA, self._handler_connected_get_metadata)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET, self._handler_connected_get)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET, self._handler_connected_set)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.EXECUTE, self._handler_connected_execute)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_PORTS, self._handler_connected_get_ports)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECT_INSTRUMENT, self._handler_connected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT_INSTRUMENT, self._handler_disconnected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CONNECTED_INSTRUMENTS, self._handler_connected_get_connected_instruments)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_ON_PORT, self._handler_connected_turn_on_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_OFF_PORT, self._handler_connected_turn_off_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CHECKSUM, self._handler_connected_get_checksum)
Beispiel #10
0
class PlatformDriver(object):
    """
    A platform driver handles a particular platform in a platform network.
    This base class provides a common interface and supporting functionality.
    """
    def __init__(self, pnode, event_callback):
        """
        Creates a PlatformDriver instance.

        @param pnode           Root PlatformNode defining the platform network
                               rooted at this platform.
        @param event_callback  Listener of events generated by this driver
        """

        #
        # NOTE the "pnode" parameter may be not very "standard" but it is the
        # current convenient mechanism that captures the overall definition
        # of the corresponding platform (most of which coming from configuration)
        #

        assert pnode, "pnode must be given"
        assert event_callback, "event_callback parameter must be given"

        self._pnode = pnode
        self._send_event = event_callback

        self._platform_id = self._pnode.platform_id
        if self._pnode.parent:
            self._parent_platform_id = self._pnode.parent.platform_id
        else:
            self._parent_platform_id = None

        self._platform_attributes = \
            dict((a.attr_id, a.defn) for a in self._pnode.attrs.itervalues())

        if log.isEnabledFor(logging.DEBUG):
            log.debug(
                "%r: PlatformDriver constructor called: pnode:\n%s\n"
                "_platform_attributes=%s", self._platform_id,
                NetworkUtil._dump_pnode(self._pnode,
                                        include_subplatforms=False),
                self._platform_attributes)

        self._driver_config = None
        self._resource_schema = {}

        # The parameter dictionary.
        self._param_dict = {}

        # construct FSM and start it with initial state UNCONFIGURED:
        self._construct_fsm()
        self._fsm.start(PlatformDriverState.UNCONFIGURED)

    def get_resource_capabilities(self, current_state=True):
        """
        """
        res_cmds = self._fsm.get_events(current_state)
        res_cmds = self._filter_capabilities(res_cmds)
        res_params = self._param_dict.keys()

        return [res_cmds, res_params]

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

    def get_resource_state(self, *args, **kwargs):
        """
        Return the current state of the driver.
        @retval str current driver state.
        """
        return self._fsm.get_current_state()

    def get_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.GET, *args, **kwargs)

    def set_resource(self, *args, **kwargs):
        """
        """
        return self._fsm.on_event(PlatformDriverEvent.SET, *args, **kwargs)

    def execute_resource(self, resource_cmd, *args, **kwargs):
        """
        Platform agent calls this directly to trigger the execution of a
        resource command. The actual action occurs in execute.
        """
        return self._fsm.on_event(PlatformDriverEvent.EXECUTE, resource_cmd,
                                  *args, **kwargs)

    def _get_platform_attributes(self):
        """
        Gets a dict of the attribute definitions in this platform as given at
        construction time (from pnode parameter).
        """
        return self._platform_attributes

    def validate_driver_configuration(self, driver_config):
        """
        Called by configure so a subclass can perform any needed additional
        validation of the provided configuration.
        Nothing is done in this base class. Note that basic validation is
        done by PlatformAgent prior to creating/configuring the driver.

        @param driver_config Driver configuration.

        @raise PlatformDriverException Error in driver configuration.
        """
        pass

    def configure(self, driver_config):
        """
        Configures this driver. In this base class it basically
        calls validate_driver_configuration and then assigns the given
        config to self._driver_config.

        @param driver_config Driver configuration.
        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: configure: %s" %
                      (self._platform_id, str(driver_config)))

        self.validate_driver_configuration(driver_config)
        self._driver_config = driver_config
        #self._param_dict = deepcopy(self._driver_config.get('attributes',{}))

    def get_config_metadata(self):
        """
        """
        return deepcopy(self._resource_schema)

    def connect(self):
        """
        To be implemented by subclass.
        Establishes communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect(self):
        """
        To be implemented by subclass.
        Ends communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def ping(self):
        """
        To be implemented by subclass.
        Verifies communication with external platform returning "PONG" if
        this verification completes OK.

        @retval "PONG"

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Returns the values for specific attributes since a given time for
        each attribute.

        @param attrs     [(attrName, from_time), ...] desired attributes.
                         from_time Assummed to be in the format basically described by
                         pyon's get_ion_ts function, "a str representing an
                         integer number, the millis in UNIX epoch."

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict indexed by attribute name with list of (value, timestamp)
                pairs. Timestamps in same format as from_time.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def set_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Sets values for writable attributes in this platform.

        @param attrs 	[(attrName, attrValue), ...] 	List of attribute values

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict with a list of (value,timestamp) pairs for each attribute
                indicated in the input. Returned timestamps indicate the time when the
                value was set. Each timestamp is "a str representing an
                integer number, the millis in UNIX epoch" to
                align with description of pyon's get_ion_ts function.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        #
        # TODO Any needed alignment with the instrument case?
        #
        raise NotImplementedError()  #pragma: no cover

    def execute(self, cmd, *args, **kwargs):
        """
        Executes the given command.
        Subclasses can override to execute particular commands or delegate to
        its super class. However, note that this base class raises
        NotImplementedError.

        @param cmd     command
        @param args    command's args
        @param kwargs  command's kwargs

        @return  result of the execution

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def get(self, *args, **kwargs):
        """
        Gets the values of the requested attributes.
        Subclasses can override to get particular attributes and
        delegate to this base implementation to handle common attributes.

        @param args    get's args
        @param kwargs  get's kwargs

        @return  result of the retrieval.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  # pragma: no cover

    def destroy(self):
        """
        Stops all activity done by the driver. Nothing done in this class.
        """
        pass

    def _notify_driver_event(self, driver_event):
        """
        Convenience method for subclasses to send a driver event to
        corresponding platform agent.

        @param driver_event a DriverEvent object.
        """
        log.debug("%r: _notify_driver_event: %s", self._platform_id,
                  driver_event)

        assert isinstance(driver_event, DriverEvent)

        self._send_event(driver_event)

    def get_external_checksum(self):
        """
        To be implemented by subclass.
        Returns the checksum of the external platform associated with this
        driver.

        @return SHA1 hash value as string of hexadecimal digits.

        @raise PlatformConnectionException  If the connection to the external
               platform is lost.
        """
        raise NotImplementedError()  #pragma: no cover

    def get_driver_state(self):
        """
        Returns the current FSM state.
        """
        return self._fsm.get_current_state()

    #####################################################################
    # Supporting method for handling connection lost in CONNECT handlers
    #####################################################################

    def _connection_lost(self, cmd, args, kwargs, exc=None):
        """
        Supporting method to be called by any CONNECTED handler right after
        detecting that the connection with the external platform device has
        been lost. It does a regular disconnect() and notifies the agent about
        the lost connection. Note that the call to disconnect() itself may
        throw some additional exception very likely caused by the fact that
        the connection is lost--this exception is just logged out but ignored.

        All parameters are for logging purposes.

        @param cmd     string indicating the command that was attempted
        @param args    args of the command that was attempted
        @param kwargs  kwargs of the command that was attempted
        @param exc     associated exception (if any),

        @return (next_state, result) suitable as the return of the FSM
                handler where the connection lost was detected. The
                next_state will always be PlatformDriverState.DISCONNECTED.
        """
        log.debug(
            "%r: (LC) _connection_lost: cmd=%s, args=%s, kwargs=%s, exc=%s",
            self._platform_id, cmd, args, kwargs, exc)

        # NOTE I have this import here as a quick way to avoid circular imports
        # (note that platform_agent also imports elements in this module)
        # TODO better to move some basic definitions to separate modules.
        from ion.agents.platform.platform_agent import PlatformAgentEvent

        result = None
        try:
            result = self.disconnect()

        except Exception as e:
            # just log a message
            log.debug(
                "%r: (LC) ignoring exception while calling disconnect upon"
                " lost connection: %s", self._platform_id, e)

        # in any case, notify the agent about the lost connection and
        # transition to DISCONNECTED:
        self._notify_driver_event(
            AsyncAgentEvent(PlatformAgentEvent.LOST_CONNECTION))

        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    ##############################################################
    # FSM event handlers.
    ##############################################################

    def _common_state_enter(self, *args, **kwargs):
        """
        Common work upon every state entry.
        """
        state = self.get_driver_state()
        log.debug('%r: driver entering state: %s', self._platform_id, state)

        self._notify_driver_event(StateChangeDriverEvent(state))

    def _common_state_exit(self, *args, **kwargs):
        """
        Common work upon every state exit.
        Nothing done in this base class.
        """

    ##############################################################
    # UNCONFIGURED event handlers.
    ##############################################################

    def _handler_unconfigured_configure(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        driver_config = kwargs.get('driver_config', None)
        if driver_config is None:
            raise FSMError('configure: missing driver_config argument')

        try:
            result = self.configure(driver_config)
            next_state = PlatformDriverState.DISCONNECTED
        except PlatformDriverException as e:
            result = None
            next_state = None
            log.error("Error in platform driver configuration", e)

        return next_state, result

    ##############################################################
    # DISCONNECTED event handlers.
    ##############################################################

    def _handler_disconnected_connect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        self.connect()
        result = next_state = PlatformDriverState.CONNECTED

        return next_state, result

    def _handler_disconnected_disconnect(self, *args, **kwargs):
        """
        We allow the DISCONNECT event in DISCONNECTED state for convenience,
        in particular it facilitates the overall handling of the connection_lost
        event, which is processed by a subsequent call to disconnect from the
        platform agent. The handler here does nothing.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        return None, None

    ###########################################################################
    # CONNECTED event handlers.
    # Except for the explicit disconnect and connection_lost handlers, the
    # CONNECTED handlers (here and in subclasses) should directly catch any
    # PlatformConnectionException to call _connection_lost.
    ###########################################################################

    def _handler_connected_disconnect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        result = self.disconnect()
        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    def _handler_connected_connection_lost(self, *args, **kwargs):
        """
        The connection was lost (as opposed to a normal disconnect request).
        Here we do the regular disconnect but also notify the platform agent
        about the lost connection.

        NOTE: this handler in the FSM is provided in case there is a need to
        directly trigger the associated transition along with the associated
        notification to the agent. However, the typical case is that a CONNECTED
        handler dealing with commands will catch any PlatformConnectionException
        to call _connection_lost directly.
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        # just use our supporting method:
        return self._connection_lost(PlatformDriverEvent.CONNECTION_LOST, args,
                                     kwargs)

    def _handler_connected_ping(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        try:
            result = self.ping()
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.PING, args,
                                         kwargs, e)

    def _handler_connected_get(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        try:
            result = self.get(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.GET, args, kwargs,
                                         e)

    def _handler_connected_set(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('set_attribute_values: missing attrs argument')

        try:
            result = self.set_attribute_values(attrs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.SET, args, kwargs,
                                         e)

    def _handler_connected_execute(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" %
                      (self._platform_id, self.get_driver_state(), str(args),
                       str(kwargs)))

        if len(args) == 0:
            raise FSMError('execute_resource: missing resource_cmd argument')

        try:
            result = self.execute(*args, **kwargs)
            return None, result

        except PlatformConnectionException as e:
            return self._connection_lost(PlatformDriverEvent.EXECUTE, args,
                                         kwargs, e)

    ##############################################################
    # Platform driver FSM setup
    ##############################################################

    def _construct_fsm(self,
                       states=PlatformDriverState,
                       events=PlatformDriverEvent,
                       enter_event=PlatformDriverEvent.ENTER,
                       exit_event=PlatformDriverEvent.EXIT):
        """
        Constructs the FSM for the driver. The preparations here are mostly
        related with the UNCONFIGURED, DISCONNECTED, and CONNECTED state
        transitions, with some common handlers for the CONNECTED state.
        Subclasses can override to indicate specific parameters and add new
        handlers (typically for the CONNECTED state).
        """
        log.debug("constructing base platform driver FSM")

        self._fsm = ThreadSafeFSM(states, events, enter_event, exit_event)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, enter_event, self._common_state_enter)
            self._fsm.add_handler(state, exit_event, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED,
                              PlatformDriverEvent.CONFIGURE,
                              self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED,
                              PlatformDriverEvent.CONNECT,
                              self._handler_disconnected_connect)
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED,
                              PlatformDriverEvent.DISCONNECT,
                              self._handler_disconnected_disconnect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.DISCONNECT,
                              self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.CONNECTION_LOST,
                              self._handler_connected_connection_lost)

        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.PING,
                              self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.GET,
                              self._handler_connected_get)
        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.SET,
                              self._handler_connected_set)
        self._fsm.add_handler(PlatformDriverState.CONNECTED,
                              PlatformDriverEvent.EXECUTE,
                              self._handler_connected_execute)
class PlatformDriver(object):
    """
    A platform driver handles a particular platform in a platform network.
    """

    def __init__(self, pnode, evt_recv):
        """
        Creates a PlatformDriver instance.

        @param pnode     Root PlatformNode defining the platform network rooted at
                         this platform.
        @param evt_recv  Listener of events generated by this driver
        """
        assert pnode, "pnode must be given"
        assert evt_recv, "evt_recv parameter must be given"

        self._pnode = pnode
        self._send_event = evt_recv

        self._platform_id = self._pnode.platform_id
        if self._pnode.parent:
            self._parent_platform_id = self._pnode.parent.platform_id
        else:
            self._parent_platform_id = None

        self._platform_attributes = \
            dict((a.attr_id, a.defn) for a in self._pnode.attrs.itervalues())

        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: PlatformDriver constructor called: pnode:\n%s\n"
                      "_platform_attributes=%s",
                      self._platform_id,
                      NetworkUtil._dump_pnode(self._pnode, include_subplatforms=False),
                      self._platform_attributes)

        self._driver_config = None

        # construct FSM and start it with initial state UNCONFIGURED:
        self._construct_fsm()
        self._fsm.start(PlatformDriverState.UNCONFIGURED)

    def _get_platform_attributes(self):
        """
        Gets a dict of the attribute definitions in this platform as given at
        construction time (from pnode parameter).
        """
        return self._platform_attributes

    def _validate_driver_configuration(self, driver_config):
        """
        Called by configure so a subclass can perform any needed additional
        validation of the provided configuration.
        Nothing is done in this base class. Note that basic validation is
        done by PlatformAgent prior to creating/configuring the driver.

        @param driver_config Driver configuration.

        @raise PlatformDriverException Error in driver configuration.
        """

    def configure(self, driver_config):
        """
        Configures this driver. It first calls _validate_driver_configuration.

        @param driver_config Driver configuration.
        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug("%r: configure: %s" % (self._platform_id, str(driver_config)))

        self._validate_driver_configuration(driver_config)
        self._driver_config = driver_config

    def connect(self):
        """
        To be implemented by subclass.
        Establishes communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect(self):
        """
        To be implemented by subclass.
        Ends communication with the platform device.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def ping(self):
        """
        To be implemented by subclass.
        Verifies communication with external platform returning "PONG" if
        this verification completes OK.

        @retval "PONG"
        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_metadata(self):
        """
        To be implemented by subclass.
        Returns the metadata associated to the platform.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Returns the values for specific attributes since a given time for
        each attribute.

        @param attrs     [(attrName, from_time), ...] desired attributes.
                         from_time Assummed to be in the format basically described by
                         pyon's get_ion_ts function, "a str representing an
                         integer number, the millis in UNIX epoch."

        @retval {attrName : [(attrValue, timestamp), ...], ...}
                dict indexed by attribute name with list of (value, timestamp)
                pairs. Timestamps in same format as from_time.
        """
        raise NotImplementedError()  #pragma: no cover

    def set_attribute_values(self, attrs):
        """
        To be implemented by subclass.
        Sets values for writable attributes in this platform.

        @param attrs 	[(attrName, attrValue), ...] 	List of attribute values

        @retval {platform_id: {attrName : [(attrValue, timestamp), ...], ...}}
                dict with a single entry for the requested platform ID and value
                as a list of (value,timestamp) pairs for each attribute indicated
                in the input. Returned timestamps indicate the time when the
                value was set. Each timestamp is "a str representing an
                integer number, the millis in UNIX epoch;" this is to be
                aligned with description of pyon's get_ion_ts function.
        """
        raise NotImplementedError()  #pragma: no cover

    def connect_instrument(self, port_id, instrument_id, attributes):
        """
        To be implemented by subclass.
        Connects an instrument to a port in this platform.

        @param port_id      Port ID
        @param instrument_id Instrument ID
        @param attributes   Attribute dictionary

        @retval The resulting configuration for the instrument.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def disconnect_instrument(self, port_id, instrument_id):
        """
        To be implemented by subclass.
        Disconnects an instrument from a port in this platform.

        @param port_id      Port ID
        @param instrument_id Instrument ID

        @retval

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_connected_instruments(self, port_id):
        """
        To be implemented by subclass.
        Retrieves the IDs of the instruments connected to a port.

        @param port_id      Port ID

        @retval

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def turn_on_port(self, port_id):
        """
        To be implemented by subclass.
        Turns on a port in this platform.

        @param port_id      Port ID

        @retval The resulting on/off of the port.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def turn_off_port(self, port_id):
        """
        To be implemented by subclass.
        Turns off a port in this platform.

        @param port_id      Port ID

        @retval The resulting on/off of the port.

        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def destroy(self):
        """
        Stops all activity done by the driver. Nothing done in this class.
        (previously it stopped resource monitoring).
        """

    def _notify_driver_event(self, driver_event):
        """
        Convenience method for subclasses to send a driver event to
        corresponding platform agent.

        @param driver_event a DriverEvent object.
        """
        log.debug("platform driver=%r: notify driver_event=%s",
            self._platform_id, driver_event)

        assert isinstance(driver_event, DriverEvent)

        self._send_event(driver_event)

    def get_checksum(self):
        """
        To be implemented by subclass.
        Returns the checksum for this platform.

        @return SHA1 hash value as string of hexadecimal digits.
        @raise PlatformConnectionException
        """
        raise NotImplementedError()  #pragma: no cover

    def get_driver_state(self):
        """
        Returns the current FSM state.
        """
        return self._fsm.get_current_state()

    ##############################################################
    # FSM event handlers.
    ##############################################################

    def _common_state_enter(self, *args, **kwargs):
        """
        Common work upon every state entry.
        Nothing done in this base class.
        @todo determine what should be done, in particular regarding eventual
        notification of the platform driver state transition.
        """
        state = self.get_driver_state()
        if log.isEnabledFor(logging.DEBUG):
            log.debug('%r: driver entering state: %s' % (self._platform_id, state))

        # TODO: publish the platform driver FSM state transition?
        # event_data = {
        #     'state': state
        # }
        # result = self._event_publisher.publish_event(
        #     event_type = ?????,
        #     origin     = ?????,
        #     **event_data)
        # if log.isEnabledFor(logging.DEBUG):
        #     log.debug('%r: PlatformDriver published state change: %s, '
        #               'time: %s result: %s',
        #               self._platform_id, state, get_ion_ts(), str(result))

    def _common_state_exit(self, *args, **kwargs):
        """
        Common work upon every state exit.
        Nothing done in this base class.
        """

    ##############################################################
    # UNCONFIGURED event handlers.
    ##############################################################

    def _handler_unconfigured_configure(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        driver_config = kwargs.get('driver_config', None)
        if driver_config is None:
            raise FSMError('configure: missing driver_config argument')

        try:
            result = self.configure(driver_config)
            next_state = PlatformDriverState.DISCONNECTED
        except PlatformDriverException as e:
            result = None
            next_state = None
            log.error("Error in platform driver configuration", e)

        return next_state, result

    ##############################################################
    # DISCONNECTED event handlers.
    ##############################################################

    def _handler_disconnected_connect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.connect()
        next_state = PlatformDriverState.CONNECTED

        return next_state, result

    ##############################################################
    # CONNECTED event handlers.
    ##############################################################

    def _handler_connected_disconnect(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.disconnect(*args, **kwargs)
        next_state = PlatformDriverState.DISCONNECTED

        return next_state, result

    def _handler_connected_ping(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = None
        try:
            result = self.ping()
            next_state = None
        except PlatformConnectionException as e:
            next_state = PlatformDriverState.DISCONNECTED
            log.error("Ping failed", e)

        return next_state, result

    def _handler_connected_get_metadata(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.get_metadata()
        next_state = None

        return next_state, result

    def _handler_connected_get_attribute_values(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('get_attribute_values: missing attrs argument')

        result = self.get_attribute_values(attrs)
        next_state = None

        return next_state, result

    def _handler_connected_set_attribute_values(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        attrs = kwargs.get('attrs', None)
        if attrs is None:
            raise FSMError('set_attribute_values: missing attrs argument')

        result = self.set_attribute_values(attrs)
        next_state = None

        return next_state, result

    def _handler_connected_connect_instrument(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('connect_instrument: missing port_id argument')

        instrument_id = kwargs.get('instrument_id', None)
        if instrument_id is None:
            raise FSMError('connect_instrument: missing instrument_id argument')

        attributes = kwargs.get('attributes', None)
        if attributes is None:
            raise FSMError('connect_instrument: missing attributes argument')

        result = self.connect_instrument(port_id, instrument_id, attributes)
        next_state = None

        return next_state, result

    def _handler_disconnected_connect_instrument(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('disconnect_instrument: missing port_id argument')

        instrument_id = kwargs.get('instrument_id', None)
        if instrument_id is None:
            raise FSMError('disconnect_instrument: missing instrument_id argument')

        result = self.disconnect_instrument(port_id, instrument_id)
        next_state = None

        return next_state, result

    def _handler_connected_get_connected_instruments(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('get_connected_instruments: missing port_id argument')

        result = self.get_connected_instruments(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_turn_on_port(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('turn_on_port: missing port_id argument')

        result = self.turn_on_port(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_turn_off_port(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        port_id = kwargs.get('port_id', None)
        if port_id is None:
            raise FSMError('turn_off_port: missing port_id argument')

        result = self.turn_off_port(port_id)
        next_state = None

        return next_state, result

    def _handler_connected_get_checksum(self, *args, **kwargs):
        """
        """
        if log.isEnabledFor(logging.TRACE):  # pragma: no cover
            log.trace("%r/%s args=%s kwargs=%s" % (
                      self._platform_id, self.get_driver_state(),
                      str(args), str(kwargs)))

        result = self.get_checksum()
        next_state = None

        return next_state, result

    ##############################################################
    # Platform driver FSM setup
    ##############################################################

    def _construct_fsm(self):
        """
        """
        log.debug("constructing fsm")

        self._fsm = ThreadSafeFSM(PlatformDriverState,
                                  PlatformDriverEvent,
                                  PlatformDriverEvent.ENTER,
                                  PlatformDriverEvent.EXIT)

        for state in PlatformDriverState.list():
            self._fsm.add_handler(state, PlatformDriverEvent.ENTER, self._common_state_enter)
            self._fsm.add_handler(state, PlatformDriverEvent.EXIT, self._common_state_exit)

        # UNCONFIGURED state event handlers:
        self._fsm.add_handler(PlatformDriverState.UNCONFIGURED, PlatformDriverEvent.CONFIGURE, self._handler_unconfigured_configure)

        # DISCONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.DISCONNECTED, PlatformDriverEvent.CONNECT, self._handler_disconnected_connect)

        # CONNECTED state event handlers:
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT, self._handler_connected_disconnect)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECTION_LOST, self._handler_connected_disconnect)

        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.PING, self._handler_connected_ping)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_METADATA, self._handler_connected_get_metadata)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_ATTRIBUTE_VALUES, self._handler_connected_get_attribute_values)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.SET_ATTRIBUTE_VALUES, self._handler_connected_set_attribute_values)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.CONNECT_INSTRUMENT, self._handler_connected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.DISCONNECT_INSTRUMENT, self._handler_disconnected_connect_instrument)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CONNECTED_INSTRUMENTS, self._handler_connected_get_connected_instruments)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_ON_PORT, self._handler_connected_turn_on_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.TURN_OFF_PORT, self._handler_connected_turn_off_port)
        self._fsm.add_handler(PlatformDriverState.CONNECTED, PlatformDriverEvent.GET_CHECKSUM, self._handler_connected_get_checksum)