Search code examples
c++qtstate-machineqtwidgetsscxml

How to use conditional transitions in Qt SCXML statecharts


I'm currently trying to understand Qts scxml state charts, and how to properly integrate them in my applications. One problem I stumbled uppon are conditional transitions. To explain how I use conditions here and what goes wrong, I made a minimum viable example:

state chart visualization

The initial state s_initial has two transitions to the states s_false and s_true. Both transitions are triggered by the same event t_button_clicked. Depending on the variable test_var, only one transition is possible possible at any time. When another t_button_clicked event occurs, the state machine returns to s_initial.

To test the state machine I created a simple Qt-Widgets application with one push-button to trigger t_button_clicked, and a checkbox to change the variable test_var:

(mainwindow.cpp)

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , chart(this)
{
    ui->setupUi(this);
    connect(ui->checkBox, &QCheckBox::clicked, [this](bool checked){
        qDebug() << "> checkbox:" << checked;
        chart.dataModel()->setProperty("test_var", checked);
    });
    connect(ui->pushButton, &QPushButton::released, [this](){
        qDebug() << "> button";
        chart.submitEvent("t_button_clicked");
    });
    chart.start();
}

(testchart.scxml)

<?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="TestChart" qt:editorversion="4.14.1" datamodel="ecmascript" initial="s_initial">
    <qt:editorinfo initialGeometry="213.11;86.67;-20;-20;40;40"/>
    <state id="s_initial">
        <qt:editorinfo scenegeometry="213.11;233.50;153.11;183.50;120;100" geometry="213.11;233.50;-60;-50;120;100"/>
        <transition type="external" event="t_button_clicked" target="s_false" cond="!test_var">
            <qt:editorinfo endTargetFactors="11.79;50.87"/>
        </transition>
        <transition type="external" event="t_button_clicked" target="s_true" cond="test_var">
            <qt:editorinfo movePoint="37.73;-3.06" endTargetFactors="19.26;54.39"/>
        </transition>
        <onentry>
            <log expr="&quot;s_initial&quot;"/>
        </onentry>
    </state>
    <state id="s_false">
        <qt:editorinfo scenegeometry="529.21;233.50;469.21;183.50;120;100" geometry="529.21;233.50;-60;-50;120;100"/>
        <onentry>
            <log expr="&quot;s_false&quot;"/>
        </onentry>
        <transition type="external" event="t_button_clicked" target="s_initial">
            <qt:editorinfo movePoint="3.06;9.18" endTargetFactors="88.28;64.08" startTargetFactors="13.98;61.45"/>
        </transition>
    </state>
    <state id="s_true">
        <qt:editorinfo scenegeometry="529.21;419.09;469.21;369.09;120;100" geometry="529.21;419.09;-60;-50;120;100"/>
        <onentry>
            <log expr="&quot;s_true&quot;"/>
        </onentry>
        <transition type="external" event="t_button_clicked" target="s_initial">
            <qt:editorinfo movePoint="-37.73;6.12" endTargetFactors="68.74;85.18" startTargetFactors="14.04;72.02"/>
        </transition>
    </state>
    <datamodel>
        <data id="test_var" expr="false"/>
    </datamodel>
</scxml>

As can be seen in the scxml-file, I added logging output to each state-onentry to see whether the state was entered or not. Additionally, I added debugging output to the button - and checkbox-clicks. When I run the application, the console output is not what I would've expected:

scxml.statemachine: "" : "s_initial"
> checkbox: false
> button
scxml.statemachine: "" : "s_false"
> button
scxml.statemachine: "" : "s_initial"
> checkbox: true
> button
scxml.statemachine: "" : "s_false"
> button
scxml.statemachine: "" : "s_initial"
> checkbox: false
> button
scxml.statemachine: "" : "s_false"
> button
scxml.statemachine: "" : "s_initial"

It doesn't matter what the value of test_var is. The state machine always takes the first transition to s_false, not checking the condition guards I added. As far as I can tell, I used valid ecmascript expressions in my chart and scxml should be able to choose the right transition according to its specification. What am I doing wrong?


Solution

    1. You should use setScxmlProperty instead of setProperty

      chart.dataModel()->setScxmlProperty("test_var", checked, "");

    2. And you may simply use _event.data for passing checkBox current value.

      chart.submitEvent("t_button_clicked", ui->checkBox->checked() ? 1:0 );

    <scxml datamodel="ecmascript" initial="s_initial" name="TestChart" version="1.0" xmlns="http://www.w3.org/2005/07/scxml">
        <state id="s_initial">
            <onentry>
                <log expr="'s_initial'"/>
            </onentry>
            <transition cond="_event.data==1" event="t_button_clicked" target="s_true"/>
            <transition event="t_button_clicked" target="s_false"/>
        </state>
        <state id="s_false">
            <onentry>
                <log expr="'s_false'"/>
            </onentry>
            <transition cond="_event.data==1" event="t_button_clicked" target="s_true"/>
        </state>
        <state id="s_true">
            <onentry>
                <log expr="'s_true'"/>
            </onentry>
            <transition cond="! (_event.data==1)" event="t_button_clicked" target="s_false"/>
        </state>
    </scxml>
    

    statechart

    P.S. You may use the next reference materials for better understanding SCXML (I'm promoting my own website)