I really like Qt's Declarative State Machine Framework (DSMF), but the way I'd like to use it is (possibly) a bit strange. I've been using Qt quite a lot in non-graphical applications on small(ish) embedded devices for the past few years, and I've recently been learning about the DSMF, which has a syntax that I really like.
The 'declarative' qualifier in the framework's name seems to mean "declared in a QML file", like so:
StateMachine {
id: stateMachine
initialState: s1
running: true
State {
id: s1
SignalTransition {
targetState: s2
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s1 entered")
onExited: console.log("s1 exited")
}
State {
// create a transition from s2 to s3 when the button is clicked
SignalTransition {
targetState: s1
signal: button.clicked
}
onEntered: console.log("s2 entered")
onExited: console.log("s2 exited")
}
}
If I've understood the framework correctly, it appears I can define the state logic entirely within a (dynamically-loaded) QML file, and achieve any side effects on the world that I need to by dinking properties of C++ objects exposed to QML via, e.g., QQmlContext::setContextProperty(). If I'm right about this, I should be able to change the state machine logic by simply modifying the QML file, without any need to recompile the app.
The difficulty I'm having is that I've only recently begun learning QML, and most (all?) of the examples I've found thus far are for graphical applications. I initially thought that QML was only intended for use in graphical applications, but this SO answer pointed me toward QBS, which looks to be a console application that uses QML.
I'm now trying to pick apart QBS to see if it provides any clues as to how I might go about using the Declarative State Machine Framework in a console application. But it'll take me awhile to figure out, and I worry there may be gotchas along the way that make achieving my goal impossible.
Can anybody confirm whether it is even possible to use the DSMF in a Qt console application? (Extra weight will be given to answers that point to—or include—specific examples...)
Since there have been no responses thus far, I'll submit my own answer to the question of whether it's possible to use the DSMF in non-graphical applications. It is! I'll present the small example program I wrote to satisfy my curiosity. First, here's a C++ class meant to simulate a 'domain object' that knows how to poke the world to achieve some desired side effect—in this case, connecting to a network:
// File: NetworkManager.h
#ifndef NETWORK_MANAGER_H
#define NETWORK_MANAGER_H
#include <QObject>
#include <QTimer>
#include <iostream>
class NetworkManager : public QObject
{
Q_OBJECT
Q_PROPERTY(bool timedOut MEMBER timedOut_ READ getTimedOut)
public:
NetworkManager() : timedOut_(false) { }
~NetworkManager() { }
Q_INVOKABLE void connectAsync()
{
std::cout << "NetworkManager: In connectAsync(), connecting...\n";
// Simulate a successful connection within 1 to 5 seconds...
QTimer::singleShot(
(1 + qrand() % 5)*1000,
[this]() {
std::cout << "NetworkManager: Connected!\n";
emit this->connected();
// ... and a random disconnect 5 to 15 seconds later.
QTimer::singleShot(
(5 + qrand() % 11)*1000,
[this]() {
std::cout << "NetworkManager: Lost connection!\n";
emit this->disconnected();
}
);
}
);
}
bool getTimedOut() const { return timedOut_; }
signals:
void connected();
void disconnected();
private:
bool timedOut_;
};
#endif // NETWORK_MANAGER_H
And here's the QML file defining the state logic. Note how it delegates the 'real' work to the domain object:
// File: NetworkManager.qml
import QtQuick 2.0
import QtQml.StateMachine 1.0
StateMachine {
id: networkFsm
initialState: sConnecting
running: true
State {
id: sConnecting
SignalTransition {
targetState: sConnected
signal: network_.connected
guard: {
network_.timedOut == false;
}
}
onEntered: {
console.log("NetworkFsm: sConnecting entered");
network_.connectAsync();
}
onExited: {
console.log("NetworkFsm: sConnecting exited");
}
}
State {
id: sConnected
SignalTransition {
targetState: sConnecting
signal: network_.disconnected
}
onEntered: {
console.log("NetworkFsm: sConnected entered");
}
onExited: {
console.log("NetworkFsm: sConnected exited");
}
}
}
Finally, here's the main.cpp file that binds the two together, and a CMakeLists.txt file to build everything:
// File: main.cpp
#include <QCoreApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <iostream>
#include "NetworkManager.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
NetworkManager network;
QQmlApplicationEngine engine;
QQmlContext* context = engine.rootContext();
context->setContextProperty("network_", &network);
engine.load(QUrl("./NetworkManagerFsm.qml"));
return app.exec();
}
# File: CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(qmlfsm)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt5Core REQUIRED)
find_package(Qt5Qml REQUIRED)
add_executable(qmlfsm main.cpp NetworkManager.h)
target_link_libraries(qmlfsm Qt5::Core Qt5::Qml)
Here's what it looks like when I run it:
qml: NetworkFsm: sConnecting entered
NetworkManager: In connectAsync(), connecting...
NetworkManager: Connected!
qml: NetworkFsm: sConnecting exited
qml: NetworkFsm: sConnected entered
NetworkManager: Lost connection!
qml: NetworkFsm: sConnected exited
qml: NetworkFsm: sConnecting entered
NetworkManager: In connectAsync(), connecting...
NetworkManager: Connected!
qml: NetworkFsm: sConnecting exited
qml: NetworkFsm: sConnected entered
^C
So, yes... it's possible. Whether or not it's useful remains to be seen. My motivation for asking the question in the first place was to see if I could make writing state machines in Qt more similar to my experience using SMC—a tool that has served me well for several years now. I love the succinctness of SMC's .sm file format, and the fact it isn't littered with <qt:editorinfo> tags and other XML bloat. The DSMF seems to hit most of those same sweet spots. Time will tell, but... I think I'll prefer it to SCXML—at least, for less complex FSMs.
NOTE
Please understand that the FSM posted above is just a 'toy' example cobbled together to demonstrate that the framework can be used in non-graphical apps. In particular, note that the
timedOutproperty of theNetworkManagerclass is only present to highlight the fact that properties ofQObjectclasses exported to QML can be used in guard conditions. (In a real implementation,timedOutwould make more sense as a separateSignalTransition.)Parameters passed to the signal that causes the transition can also be used in guard conditions, but I personally feel that the FSM 'reads better' when properties/methods of the domain object are accessed explicitly. Otherwise, one wonders where, e.g., the
timedOutparameter has come from, and must open the header file for the domain object's class in order to be certain.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With