I have the following state machine:
I have implemented this in Python via the standard state pattern. This state machine is running in a separate thread from a PySide6 application thread, which is running on the program's main thread. Let's call this state machine class Controller
.
On the GUI side, I am using the Qt for Python State Machine Framework. (Note: the linked documentation is for PySide2/PySide5, but I am using Pyside6. For some reason, the same documentation doesn't exist for PySide6.) This makes it very nice to define what happens in the GUI when a state is entered.
What I am looking for is not for the GUI to initiate transitions but for my core state machine Controller
running in another thread to control the transitions. Typically, one uses <QState>.addTransition(...)
to add a transition at the GUI level, but I want the GUI to simple send commands down to Controller
, and then when Controller
transitions its state, I want it to emit a Signal
that triggers the PySide6 QStateMachine
to enter a given state and thus set all the properties appropriate for that state. So in other words, I want the GUI to dispatch commands to the Controller
state machine and for the GUI to "listen" for the Controller
's state transitions.
So the question is, given a QState
, how do I send a signal to it that forces the QStateMachine
it is a part of to transition to that state? Or do I need to send a signal to the QStateMachine
and provide it a QState
to transition to?
Example program:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import override
from PySide6.QtStateMachine import QState
class Controller:
def __init__(self, initial_state: IState, states: list[QState]) -> None:
"""Initialize the controller to the given initial state and call the `
on_entry` method for the state.
"""
self.__state = initial_state
self.__state.controller = self
self.__state.on_entry()
self.__state.states = states
def _transition_to(self, new_state: IState) -> None:
"""Transition from the current state to the given new state. This calls
the `on_exit` method on the current state and the `on_entry` of the
new state. This method should not be called by any object other than
concrete implementations of `IState`.
"""
self.__state.on_exit()
self.__state = new_state
self.__state.controller = self
self.__state.on_entry()
@property
def state(self):
"""Get the current state"""
return self.__state
def start_camera_exposure(self) -> None:
self.__state.start_camera_exposure()
def stop_camera_exposure(self) -> None:
self.__state.stop_camera_exposure()
def abort_camera_exposure(self) -> None:
self.__state.abort_camera_exposure()
class IState(ABC):
"""Serve as an interface between the controller and the explicit, individual states."""
@property
def controller(self) -> Controller:
return self.__controller
@controller.setter
def controller(self, controller: Controller) -> None:
self.__controller = controller
@property
def states(self) -> list[QState]:
return self.__states
@states.setter
def states(self, states: list[QState]):
self.__states = states
def on_entry(self) -> None:
"""Can be overridden by a state to perform an action when the state is
being entered, i.e., transitions into. It is not required to be overridden.
"""
pass
def on_exit(self) -> None:
"""Can be overridden by a state to perform an action when the state is
being exited, i.e., transitioned from. It is not required to be overridden.
"""
pass
# If a concrete implementation does not handle the called method, i.e., it is an invalid action
# in the specific state, it is enough to simply call `pass`.
@abstractmethod
def start_camera_exposure(self) -> None: ...
@abstractmethod
def stop_camera_exposure(self) -> None: ...
@abstractmethod
def abort_camera_exposure(self) -> None: ...
class Idle(IState):
@override
def on_entry(self):
# I want to emit a signal here to force a `QStateMachine` to go to state: `self.__states[0]`
print("Idling ...")
def start_camera_exposure(self) -> None:
self.controller._transition_to(CameraExposing())
def stop_camera_exposure(self) -> None:
pass
def abort_camera_exposure(self) -> None:
pass
class CameraExposing(IState):
@override
def on_entry(self) -> None:
# I want to emit a signal here to force a `QStateMachine` to go to state: `self.__states[1]`
print("Starting camera exposure ...")
@override
def on_exit(self) -> None:
print("Stopping camera exposure ...")
def start_camera_exposure(self) -> None:
pass
def stop_camera_exposure(self) -> None:
self.controller._transition_to(SavingCameraImages())
def abort_camera_exposure(self) -> None:
self.controller._transition_to(AbortingCameraExposure())
class SavingCameraImages(IState):
@override
def on_entry(self) -> None:
# I want to emit a signal here to force a `QStateMachine` to go to state: `self.__states[2]`
print("Saving camera images ...")
self.controller._transition_to(Idle())
def start_camera_exposure(self) -> None:
pass
def stop_camera_exposure(self) -> None:
pass
def abort_camera_exposure(self) -> None:
pass
class AbortingCameraExposure(IState):
@override
def on_entry(self) -> None:
# I want to emit a signal here to force a `QStateMachine` to go to state: `self.__states[3]`
print("Aborting camera exposure ...")
self.controller._transition_to(Idle())
def start_camera_exposure(self) -> None:
pass
def stop_camera_exposure(self) -> None:
pass
def abort_camera_exposure(self) -> None:
pass
On the GUI side, I have something like:
from PySide6.QtStateMachine import QState, QStateMachine
class MainWindow(QWidget):
def __init__(self) -> None:
super().__init__()
self.machine = QStateMachine(parent=self)
self.state_idle = QState(self.machine)
self.state_camera_exposing = QState(self.machine)
self.state_saving_camera_images = QState(self.machine)
self.state_aborting_camera_exposure = QState(self.machine)
self.machine.setInitialState(self.state_idle)
self.states = [
self.state_idle,
self.state_camera_exposing,
self.state_saving_camera_images,
self.state_aborting_camera_exposure,
]
self.initialize()
The only way that I have so far figured out how to do this is by simply creating adding all possible transitions for every transition signal for every state.
Inside class MainWindow(QWidget)
, define class variables to represent the various transition signals:
transition_to_idle = Signal()
transition_to_camera_exposing = Signal()
transition_to_saving_camera_images = Signal()
transition_to_aborting_camera_exposure = Signal()
Then add transitions for each of these signals for each state, transitioning to the state determined by the signal:
machine = self.machine
state_idle = self.state_idle
state_camera_exposing = self.state_camera_exposing
state_saving_camera_images = self.state_saving_camera_images
state_aborting_camera_exposure = self.state_aborting_camera_exposure
state_idle.addTransition(self.transition_to_idle, state_idle)
state_idle.addTransition(self.transition_to_camera_exposing, state_camera_exposing)
state_idle.addTransition(self.transition_to_saving_camera_images, state_saving_camera_images)
state_idle.addTransition(self.transition_to_aborting_camera_exposure, state_aborting_camera_exposure)
state_camera_exposing.addTransition(self.transition_to_idle, state_idle)
state_camera_exposing.addTransition(self.transition_to_camera_exposing, state_camera_exposing)
state_camera_exposing.addTransition(self.transition_to_saving_camera_images, state_saving_camera_images)
state_camera_exposing.addTransition(self.transition_to_aborting_camera_exposure, state_aborting_camera_exposure)
state_saving_camera_images.addTransition(self.transition_to_idle, state_idle)
state_saving_camera_images.addTransition(self.transition_to_camera_exposing, state_camera_exposing)
state_saving_camera_images.addTransition(self.transition_to_saving_camera_images, state_saving_camera_images)
state_saving_camera_images.addTransition(self.transition_to_aborting_camera_exposure, state_aborting_camera_exposure)
state_aborting_camera_exposure.addTransition(self.transition_to_idle, state_idle)
state_aborting_camera_exposure.addTransition(self.transition_to_camera_exposing, state_camera_exposing)
state_aborting_camera_exposure.addTransition(self.transition_to_saving_camera_images, state_saving_camera_images)
state_aborting_camera_exposure.addTransition(self.transition_to_aborting_camera_exposure, state_aborting_camera_exposure)
Then in the other thread, signals are emitted with emit
:
transition_to_idle.emit()
transition_to_camera_exposing.emit()
transition_to_saving_camera_images.emit()
transition_to_aborting_camera_exposure.emit()
Edit: To set the transitions more succinctly, a for loop can be used. For example:
for state in states:
state.addTransition(self.transition_to_idle, state_idle)
state.addTransition(self.transition_to_camera_exposing, state_camera_exposing)
state.addTransition(self.transition_to_saving_camera_images, state_saving_camera_images)
state.addTransition(self.transition_to_aborting_camera_exposure, state_aborting_camera_exposure)
where states
is a list of all the possible states.