Scxml module : invoke scxml dynamically
-
Could anyone indicate a working example of a scxml file loaded dynamically supporting "sub-machine" (defined with scxml files) invoked from the main machine using C++ ?
This example is the closest from what I am looking for but the submachine is defined within the main scxml whereas I would like it to be part of a separate file and it is a qml example.So far, I am able to :
- dynamicaly load a "main" scxml using QScxmlStateMachine::fromFile
- connect the scxml machine to be notified when a state change and react to onEnter onExit events
- be notified when a service is invoked and get the name of the subservice.
My problem is that I don't know how to retrieve the list of state from the invoked scxml and be notified of its event, calling stateNames on the main scxml is not returning states from the invoked service.
From the main scxml, I am defining the invoke service with this element inside a transition : <invoke type="http://www.w3.org/TR/scxml/" src="groundOperations.scxml"/>
It guess the path is correct as the scxml will complain when I set an src attribute value that does not corresponds to an existing file, .So I wondering whether I am missing something obvious here or if I miss manual process (for instance, I could create manually sub scxml machines reacting to invokedServicesChanged signal into a stack but I don't want to fight against the framework.
As an additional but related topic, I feel QScxmlStateMachine is fine to quickly get things up and running (the QtCreator editor is nice) but it is missing a few feature :
- I don't think there is a way to know the transition of the current state (except parsing the scxml file)
- there is no way to manually activate a given state
Actually I am considering creating a class having the same API as QScxmlStateMachine but internally relying on QStateMachine and parsing scxml to keep the same behavior with the missing feature.
Thanks in advance for any feedback.
-
Hi,
Can you provide a minimal compilable example that shows this behaviour ?
Silly idea, did you try to load the state machine from the same folder as were the executable is ?
-
Thanks for your reply, I will come back with a minimalistic example..
I tried to put the scxml file in the executable folder and got the same behavior : the machine shows no error (if the scxml name is incorrect or could not be found I have an error) but the states of the nested machine is not updated neither the events of the child machine are received. -
Did you check the module's tests ? There might be data there that would fit your scenario.
-
Good idea, I will review the module's test !
Here is a small compilable example :
main.cpp
#include "mainwindow.h" #include <QApplication> int main(int argc, char **argv) { QApplication app(argc, argv); MainWindow mainWindow; mainWindow.show(); return app.exec(); }
mainwindow.h
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QScxmlStateMachine> #include <QScxmlInvokableService> #include <QDebug> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); public slots: void start( bool checked); void onInvokedServicesChanged(const QVector<QScxmlInvokableService *> & lstSrv); void onMessageReceived(const QScxmlEvent &event); private: QScxmlStateMachine * m_pSM = nullptr; }; #endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h" #include <QPushButton> #include <QFileInfo> #include <QLayout> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QFileInfo fileScxml("main.scxml"); if (!fileScxml.exists() || !fileScxml.isReadable()) qWarning() << "scxml can't be read :" << fileScxml.absoluteFilePath(); // show scxml errors if any m_pSM = QScxmlStateMachine::fromFile( fileScxml.absoluteFilePath()); for (auto error : m_pSM->parseErrors()) qDebug() << error.toString(); // output xml log connect(m_pSM, &QScxmlStateMachine::log, [](const QString &label, const QString &msg){ qDebug() << "error ("<< label <<") : " << msg; }); connect(m_pSM, &QScxmlStateMachine::invokedServicesChanged, this, &MainWindow::onInvokedServicesChanged); m_pSM->connectToEvent("SendMessage", this, &MainWindow::onMessageReceived ); m_pSM->setParent(this); // add buttons for starting and sending events QPushButton *startButton = new QPushButton("Start"); connect(startButton, &QPushButton::clicked, this, &MainWindow::start); QPushButton *sendNext = new QPushButton("Send 'Next'(main) event"); connect(sendNext, &QPushButton::clicked, [&](){m_pSM->submitEvent("nextMain");}); QPushButton *sendNext2= new QPushButton("Send 'Next'(child) event"); connect(sendNext2, &QPushButton::clicked, [&](){m_pSM->submitEvent("nextChild");}); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(startButton); layout->addWidget(sendNext); layout->addWidget(sendNext2); QWidget* pwidget = new QWidget(this); setCentralWidget(pwidget); pwidget->setLayout(layout); } MainWindow::~MainWindow() { } void MainWindow::start(bool checked) { Q_UNUSED(checked); if (m_pSM->isRunning()) m_pSM->stop(); else m_pSM->start(); qDebug() << "isRunning :" << m_pSM->isRunning(); qDebug() << m_pSM->stateNames(false).join(";"); } void MainWindow::onInvokedServicesChanged(const QVector<QScxmlInvokableService *> &lstSrv) { for (QScxmlInvokableService* srv : lstSrv) { qDebug() << "invoked srv : "<< srv->name(); srv->start(); } qDebug() << m_pSM->stateNames(false).join(";"); // nested machines are not listed (as stated in the doc) } void MainWindow::onMessageReceived(const QScxmlEvent &event) { QVariantMap eventData = event.data().toMap(); qDebug() << "Message :" << eventData.value("Message").toString(); }
The nested machine (main.scxml inside the application dir) :
<?xml version="1.0" encoding="UTF-8"?> <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" binding="early" xmlns:qt="http://www.qt.io/2015/02/scxml-ext" name="main.scml" qt:editorversion="4.3.1" datamodel="ecmascript" initial="Main_1"> <qt:editorinfo initialGeometry="175;59.18;-20;-20;40;40"/> <state id="Main_1"> <qt:editorinfo geometry="236.52;203.85;-129.59;-50;120;100" scenegeometry="236.52;203.85;106.93;153.85;120;100"/> <invoke type="http://www.w3.org/TR/scxml/" src="child.scxml"/> <onentry> <send event="SendMessage"> <param name="Message" expr="'Entering Main_1'"/> </send> </onentry> <transition type="external" event="nextMain" target="Main_2"> <qt:editorinfo movePoint="66.60;-1.01"/> </transition> </state> <final id="Final_1"> <qt:editorinfo geometry="166.93;1000.90;-20;-20;40;40" scenegeometry="166.93;1000.90;146.93;980.90;40;40"/> </final> <state id="Main_2"> <qt:editorinfo geometry="166.93;472.22;-60;-50;120;100" scenegeometry="166.93;472.22;106.93;422.22;120;100"/> <transition type="external" event="nextMain" target="Final_1"> <qt:editorinfo startTargetFactors="43.32;71.16" movePoint="68.61;-110.99"/> </transition> <onentry> <qt:editorinfo geometry="-60;-50;0;0;0;0"/> <send event="SendMessage"> <param name="Message" expr="'Entering Main_2'"/> </send> </onentry> </state> </scxml>
the machine invoked from main (child.scxml inside the application dir)
<?xml version="1.0" encoding="UTF-8"?> <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" binding="early" xmlns:qt="http://www.qt.io/2015/02/scxml-ext" name="child.scxml" qt:editorversion="4.3.1" datamodel="ecmascript" initial="Child_1"> <qt:editorinfo initialGeometry="186.73;45.92;-20;-20;40;40"/> <state id="Child_1"> <qt:editorinfo geometry="186.73;155.10;-60;-50;120;100" scenegeometry="186.73;155.10;126.73;105.10;120;100"/> <transition type="external" event="nextChild" target="child_2"> <qt:editorinfo movePoint="72.65;5.05"/> </transition> <onentry> <send event="SendMessage"> <param name="Message" expr="'Entering Child_1'"/> </send> </onentry> </state> <state id="child_2"> <qt:editorinfo geometry="186.73;279.59;-60;-50;120;100" scenegeometry="186.73;279.59;126.73;229.59;120;100"/> <transition type="external" event="nextChild" target="child_3"> <qt:editorinfo movePoint="66.60;4.04"/> </transition> <onentry> <qt:editorinfo geometry="-60;-50;0;0;0;0"/> <send event="SendMessage"> <param name="Message" expr="'Entering Child_2'"/> </send> </onentry> </state> <state id="child_3"> <qt:editorinfo geometry="186.73;405.09;-60;-50;120;100" scenegeometry="186.73;405.09;126.73;355.09;120;100"/> <transition type="external" event="nextChild" target="child_4"> <qt:editorinfo movePoint="91.83;9.18"/> </transition> <onentry> <qt:editorinfo geometry="-60;-50;0;0;0;0"/> <send event="SendMessage"> <param name="Message" expr="'Entering Child_3'"/> </send> </onentry> </state> <state id="child_4"> <qt:editorinfo geometry="186.73;518.36;-60;-50;120;100" scenegeometry="186.73;518.36;126.73;468.36;120;100"/> <transition type="external" event="nextChild" target="Final_1"> <qt:editorinfo movePoint="89.80;4.04"/> </transition> <onentry> <qt:editorinfo geometry="-60;-50;0;0;0;0"/> <send event="SendMessage"> <param name="Message" expr="'Entering Child_4'"/> </send> </onentry> </state> <final id="Final_1"> <qt:editorinfo geometry="186.73;623.46;-20;-20;40;40" scenegeometry="186.73;623.46;166.73;603.46;40;40"/> </final> </scxml>
What the example is doing :
- load 'main.scxml' a simple state machine having 2 states, the initial one is invoking 'child.scxml'
- create 3 buttons :
- Start button => start the state machine
- Send next (main) => send the event (nextMain) which corresponds to the transitions between the main.scxml states
- Send next (child) => send the event (nextChild) which corresponds to the transitions between the child.scxml states
- Every states (defined in main or child scxml) send a SendMessage event with the state name as parameter. This event is connected to output.
One can see that when start is pressed, the main.scxml actually starts, enter main first state (SendMessage event received), invoke child.scxml (slots reacting to invokedServicesChanged is entered). But later on I don't see how to interact with the invoked service : I can't retrieve its states (as written in the documentation, stateName is not listing nested machines states), can't receive or send event to the child machine....I am surely missing something obvious !