# -*- coding: utf-8 -*- # statemachines.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ State machines for the Bitmask app. """ from PySide import QtCore from PySide.QtCore import QStateMachine, QState, Signal from PySide.QtCore import QObject from leap.bitmask.services import connections from leap.common.check import leap_assert_type from leap.bitmask.logs.utils import get_logger logger = get_logger() _tr = QObject().tr # Indexes for the state dict _ON = "on" _OFF = "off" _CON = "connecting" _DIS = "disconnecting" class SignallingState(QState): """ A state that emits a custom signal on entry. """ def __init__(self, signal, parent=None, name=None): """ Initializer. :param signal: the signal to be emitted on entry on this state. :type signal: QtCore.QSignal """ super(SignallingState, self).__init__(parent) self._signal = signal self._name = name def onEntry(self, *args): """ Emits the signal on entry. """ logger.debug('State %s::%s entered. Emitting signal ...' % (self._name, self.objectName())) if self._signal is not None: self._signal.emit() class States(object): """ States for composite objects """ class Off(SignallingState): pass class Connecting(SignallingState): pass class On(SignallingState): pass class Disconnecting(SignallingState): pass class StepsTrack(QObject): state_change = Signal() def __init__(self, target): super(States.StepsTrack, self).__init__() self.received = set([]) self.target = set(target) def is_all_done(self): return all([ev in self.target for ev in self.received]) def is_any_done(self): return any([ev in self.target for ev in self.received]) def seen(self, _type): if _type in self.target: self.received.add(_type) def reset_seen(self): self.received = set([]) class TransitionOR(QtCore.QSignalTransition): def __init__(self, state): super(States.TransitionOR, self).__init__( state, QtCore.SIGNAL('state_change()')) self.state = state def eventTest(self, e): self.state.seen(e.type()) done = self.state.is_any_done() if done: self.state.reset_seen() return done def onTransition(self, e): pass class TransitionAND(QtCore.QSignalTransition): def __init__(self, state): super(States.TransitionAND, self).__init__( state, QtCore.SIGNAL('state_change()')) self.state = state def eventTest(self, e): self.state.seen(e.type()) done = self.state.is_all_done() if done: self.state.reset_seen() return done def onTransition(self, e): pass class CompositeEvent(QtCore.QEvent): def __init__(self): super(CompositeEvent, self).__init__( QtCore.QEvent.Type(self.ID)) class Composite(object): # TODO we should generate the connectingEvents dinamycally, # depending on how much composite states do we get. # This only supports up to 2 composite states. class ConnectingEvent1(CompositeEvent): ID = QtCore.QEvent.User + 1 class ConnectingEvent2(CompositeEvent): ID = QtCore.QEvent.User + 2 class ConnectedEvent1(CompositeEvent): ID = QtCore.QEvent.User + 3 class ConnectedEvent2(CompositeEvent): ID = QtCore.QEvent.User + 4 class DisconnectingEvent1(CompositeEvent): ID = QtCore.QEvent.User + 5 class DisconnectingEvent2(CompositeEvent): ID = QtCore.QEvent.User + 6 class DisconnectedEvent1(CompositeEvent): ID = QtCore.QEvent.User + 7 class DisconnectedEvent2(CompositeEvent): ID = QtCore.QEvent.User + 8 class Events(QtCore.QObject): """ A Wrapper object for containing the events that will be posted to a composite state machine. """ def __init__(self, parent=None): """ Initializes the QObject with the given parent. """ QtCore.QObject.__init__(self, parent) class CompositeMachine(QStateMachine): def __init__(self, parent=None): QStateMachine.__init__(self, parent) # events self.events = Events(parent) self.create_events() def create_events(self): """ Creates a bunch of events to be posted to the state machine when the transitions say so. """ # XXX refactor into a dictionary? self.events.con_ev1 = Composite.ConnectingEvent1() self.events.con_ev2 = Composite.ConnectingEvent2() self.events.on_ev1 = Composite.ConnectedEvent1() self.events.on_ev2 = Composite.ConnectedEvent2() self.events.dis_ev1 = Composite.DisconnectingEvent1() self.events.dis_ev2 = Composite.DisconnectingEvent2() self.events.off_ev1 = Composite.DisconnectedEvent1() self.events.off_ev2 = Composite.DisconnectedEvent2() def beginSelectTransitions(self, e): """ Weird. Having this method makes underlying backtraces to appear magically on the transitions. :param e: the received event :type e: QEvent """ pass def _connect_children(self, child1, child2): """ Connects the state transition signals for children machines. :param child1: the first child machine :type child1: QStateMachine :param child2: the second child machine :type child2: QStateMachine """ # TODO refactor and generalize for composites # of more than 2 connections. c1 = child1.conn c1.qtsigs.connecting_signal.connect(self.con_ev1_slot) c1.qtsigs.connected_signal.connect(self.on_ev1_slot) c1.qtsigs.disconnecting_signal.connect(self.dis_ev1_slot) c1.qtsigs.disconnected_signal.connect(self.off_ev1_slot) c2 = child2.conn c2.qtsigs.connecting_signal.connect(self.con_ev2_slot) c2.qtsigs.connected_signal.connect(self.on_ev2_slot) c2.qtsigs.disconnecting_signal.connect(self.dis_ev2_slot) c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot) # XXX why is this getting deletec in c++? # Traceback (most recent call last): # self.postEvent(self.events.on_ev2) # RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. # XXX trying the following workaround, since # I cannot find why in the world this is getting deleted :( # XXX refactor! # slots connection1 def con_ev1_slot(self): # XXX if we just postEvent, we get the Internal C++ object deleted... # so the workaround is to re-create it each time. self.events.con_ev1 = Composite.ConnectingEvent1() self.postEvent(self.events.con_ev1) def on_ev1_slot(self): self.events.on_ev1 = Composite.ConnectedEvent1() self.postEvent(self.events.on_ev1) def dis_ev1_slot(self): self.events.dis_ev1 = Composite.DisconnectingEvent1() self.postEvent(self.events.dis_ev1) def off_ev1_slot(self): self.events.off_ev1 = Composite.DisconnectedEvent1() self.postEvent(self.events.off_ev1) # slots connection2 def con_ev2_slot(self): self.events.con_ev2 = Composite.ConnectingEvent2() self.postEvent(self.events.con_ev2) def on_ev2_slot(self): self.events.on_ev2 = Composite.ConnectedEvent2() self.postEvent(self.events.on_ev2) def dis_ev2_slot(self): self.events.dis_ev2 = Composite.DisconnectingEvent2() self.postEvent(self.events.dis_ev2) def off_ev2_slot(self): self.events.off_ev2 = Composite.DisconnectedEvent2() self.postEvent(self.events.off_ev2) class ConnectionMachineBuilder(object): """ Builder class for state machines made from LEAPConnections. """ def __init__(self, connection): """ :param connection: an instance of a concrete LEAPConnection we will be building a state machine for. :type connection: AbstractLEAPConnection """ self._conn = connection leap_assert_type(self._conn, connections.AbstractLEAPConnection) def make_machine(self, **kwargs): """ Creates a statemachine associated with the passed controls. It returns the state machine if the connection used for initializing the ConnectionMachineBuilder inherits exactly from LEAPAbstractConnection, and a tuple with the Composite Machine and its individual parts in case that it is a composite machine which connection definition inherits from more than one class that, on their time, inherit from LEAPAbstractConnection. :params: see parameters for ``_make_simple_machine`` :returns: a QStateMachine, or a tuple with the form: (CompositeStateMachine, (StateMachine1, StateMachine2)) :rtype: QStateMachine or tuple """ components = self._conn.components if components is None: # simple case: connection definition inherits directly from # the abstract connection. leap_assert_type(self._conn, connections.AbstractLEAPConnection) return self._make_simple_machine(self._conn, **kwargs) if components: # composite case: connection definition inherits from several # classes, each one of which inherit from the abstract connection. child_machines = tuple( [ConnectionMachineBuilder(connection()).make_machine() for connection in components]) composite_machine = self._make_composite_machine( self._conn, child_machines, **kwargs) composite_machine._connect_children( *child_machines) # XXX should also connect its own states with the signals # for the composite machine itself return (composite_machine, child_machines) def _make_composite_machine(self, conn, children, **kwargs): """ Creates a composite machine. :param conn: an instance of a connection definition. :type conn: LEAPAbstractConnection :param children: children machines :type children: tuple of state machines :returns: A composite state machine :rtype: QStateMachine """ # TODO split this method in smaller utility functions. # 1. create machine machine = CompositeMachine() # 2. create states off = States.Off(conn.qtsigs.disconnected_signal, parent=machine, name=conn.name) off.setObjectName("off") on = States.On(conn.qtsigs.connected_signal, parent=machine, name=conn.name) on.setObjectName("on") connecting_state = States.Connecting( conn.qtsigs.connecting_signal, parent=machine, name=conn.name) connecting_state.setObjectName("connecting") disconnecting_state = States.Disconnecting( conn.qtsigs.disconnecting_signal, parent=machine, name=conn.name) disconnecting_state.setObjectName("disconnecting") # 3. TODO create as many connectingEvents as needed (dynamically create # classses for that) # (we have manually created classes for events under CompositeEvent for # now, to begin with the simple 2 states case for mail. # 4. state tracking objects for each transition stage connecting_track0 = States.StepsTrack( (Composite.ConnectingEvent1.ID, Composite.ConnectingEvent2.ID)) connecting_track0.setObjectName("connecting_step_0") connecting_track1 = States.StepsTrack( (Composite.ConnectedEvent1.ID, Composite.ConnectedEvent2.ID)) connecting_track1.setObjectName("connecting_step_1") disconnecting_track0 = States.StepsTrack( (Composite.DisconnectingEvent1.ID, Composite.DisconnectingEvent2.ID)) disconnecting_track0.setObjectName("disconnecting_step_0") disconnecting_track1 = States.StepsTrack( (Composite.DisconnectedEvent1.ID, Composite.DisconnectedEvent2.ID)) disconnecting_track1.setObjectName("disconnecting_step_1") # 5. definte the transitions with the matching state-tracking # objects. # off -> connecting connecting_transition = States.TransitionOR( connecting_track0) connecting_transition.setTargetState(connecting_state) off.addTransition(connecting_transition) # connecting -> on connected_transition = States.TransitionAND( connecting_track1) connected_transition.setTargetState(on) connecting_state.addTransition(connected_transition) # on -> disconnecting disconnecting_transition = States.TransitionOR( disconnecting_track0) disconnecting_transition.setTargetState(disconnecting_state) on.addTransition(disconnecting_transition) # disconnecting -> off disconnected_transition = States.TransitionAND( disconnecting_track1) disconnected_transition.setTargetState(off) disconnecting_state.addTransition(disconnected_transition) machine.setInitialState(off) machine.conn = conn return machine def _make_simple_machine(self, conn, button=None, action=None, label=None): """ Creates a statemachine associated with the passed controls. :param conn: the connection instance that defines this machine. :type conn: AbstractLEAPConnection :param button: the switch button. :type button: QPushButton :param action: the actionh that controls connection switch in a menu. :type action: QAction :param label: the label that displays the connection state :type label: QLabel :returns: a state machine :rtype: QStateMachine """ machine = QStateMachine() states = self._make_states(conn, button, action, label) # transitions: states[_OFF].addTransition( conn.qtsigs.do_connect_signal, states[_CON]) # * Clicking the buttons or actions transitions to the # intermediate stage. if button: states[_OFF].addTransition( button.clicked, states[_CON]) states[_ON].addTransition( button.clicked, states[_DIS]) if action: states[_OFF].addTransition( action.triggered, states[_CON]) states[_ON].addTransition( action.triggered, states[_DIS]) # * We transition to the completed stages when # we receive the matching signal from the underlying # conductor. states[_CON].addTransition( conn.qtsigs.connected_signal, states[_ON]) states[_DIS].addTransition( conn.qtsigs.disconnected_signal, states[_OFF]) # * If we receive the connection_died, we transition # from on directly to the off state states[_ON].addTransition( conn.qtsigs.connection_died_signal, states[_OFF]) # XXX adding this--------------------- states[_ON].addTransition( conn.qtsigs.do_disconnect_signal, states[_DIS]) # * If we receive the connection_aborted, we transition # from connecting to the off state states[_CON].addTransition( conn.qtsigs.connection_aborted_signal, states[_OFF]) # * Connection died can in some cases also be # triggered while we are in CONNECTING # state. I should be avoided, since connection_aborted # is clearer (and reserve connection_died # for transitions from on->off states[_CON].addTransition( conn.qtsigs.connection_died_signal, states[_OFF]) # adding states to the machine for state in states.itervalues(): machine.addState(state) machine.setInitialState(states[_OFF]) machine.conn = conn return machine def _make_states(self, conn, button, action, label): """ Creates the four states for the simple state machine. Adds the needed properties for the passed controls. :param conn: the connection instance that defines this machine. :type conn: AbstractLEAPConnection :param button: the switch button. :type button: QPushButton :param action: the actionh that controls connection switch in a menu. :type action: QAction :param label: the label that displays the connection state :type label: QLabel :returns: a dict of states :rtype: dict """ states = {} # TODO add tooltip # OFF State ---------------------- off = SignallingState( None, name=conn.name) off_label = _tr("Turn {0}").format( conn.Connected.short_label) if button: off.assignProperty( button, 'text', off_label) off.assignProperty( button, 'enabled', True) off.assignProperty( button, 'visible', True) if action: off.assignProperty( action, 'text', off_label) off.assignProperty( action, 'enabled', True) off.assignProperty( action, 'visible', True) off.setObjectName(_OFF) states[_OFF] = off # CONNECTING State ---------------- connecting = SignallingState( conn.qtsigs.connecting_signal, name=conn.name) on_label = _tr("Turn {0}").format( conn.Disconnected.short_label) if button: connecting.assignProperty( button, 'text', on_label) connecting.assignProperty( button, 'enabled', False) if action: connecting.assignProperty( action, 'text', on_label) connecting.assignProperty( action, 'enabled', False) connecting.setObjectName(_CON) states[_CON] = connecting # ON State ------------------------ on = SignallingState( None, name=conn.name) on_label = _tr("Turn {0}").format( conn.Disconnected.short_label) if button: on.assignProperty( button, 'text', on_label) on.assignProperty( button, 'enabled', True) if action: on.assignProperty( action, 'text', on_label) on.assignProperty( action, 'enabled', True) # TODO set label for ON state on.setObjectName(_ON) states[_ON] = on # DISCONNECTING State ------------- disconnecting = SignallingState( conn.qtsigs.disconnecting_signal, name=conn.name) if button: disconnecting.assignProperty( button, 'enabled', False) # XXX complete disconnecting # TODO disable button disconnecting.setObjectName(_DIS) states[_DIS] = disconnecting return states