3

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...)

evadeflow
  • 4,704
  • 38
  • 51
  • What elements do you want to manipulate through DSMF? If you are using C ++ because you do not use [The State Machine Framework](http://doc.qt.io/qt-5/statemachine-api.html)? – eyllanesc Jan 04 '18 at 23:45
  • The elements I want to manipulate are C++ objects derived from QObject. I want to define the FSM logic in QML so that it’s all in one place, in an easily-readable format, and I can modify the logic without recompiling the app. – evadeflow Jan 05 '18 at 00:37

1 Answers1

1

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 timedOut property of the NetworkManager class is only present to highlight the fact that properties of QObject classes exported to QML can be used in guard conditions. (In a real implementation, timedOut would make more sense as a separate SignalTransition.)

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 timedOut parameter has come from, and must open the header file for the domain object's class in order to be certain.

evadeflow
  • 4,704
  • 38
  • 51