Qt Interface Framework Simulation System

When you develop new APIs, a service that the API requires may not exist yet. This is because, the API is already designed but the service itself is still being developed. For example, with new concepts like Autonomous Driving, the UI interaction and its API are designed, but the Autonomous Drive Service is not yet completed. This development cycle requires the need to separate the API development from the service. The Dynamic Backend System provides the architecture to enable this separation.

Once we have this separation in place, the next step for developing the API is a way to simulate its behavior to mirror the original service. The Qt Interface Framework Simulation System enables the following use cases:

  • An easy to define simulation logic in QML.
  • A flexible system to provide simulation for any C++ API.
  • A clear split between simulation data and simulation logic.
  • An override mechanism to change the simulation at runtime; useful for auto-tests.
  • An integration with the Qt Interface Framework Generator tools.

Architecture

Since the simulation system builds on top of the Dynamic Backend System, the API split follows the same schema:

"Relationship between the Feature and the Backend"

Each back-end plugin needs to implement the back-end interface to provide the necessary functionality to the front end. For example, the QIfClimateControlBackendInterface class for the QIfClimateControl front-end API.

In the back end, every call from the front end is forwarded to QML, where we can script simulation behavior.

"QtInterfaceFramework Simulation System"

QML API

The core of the Qt Interface Framework Simulation System is the QIfSimulationEngine. This engine extends QQmlApplicationEngine and provides the extra functionality to connect C++ and QML logic together.

Each back end uses its own simulation engine to separate the front-end code from the back-end QML code. To provide a binding between QML and C++ objects, the C++ instance must be registered with the QIfSimulationEngine under a certain name. For each registered C++ instance, the engine creates a proxy object and exposes it as a QML type. These QML types can be used to provide the behavior for functions or to update properties.

For example, suppose your feature has a function called increment() and a property called counter. The actual behavior for increment() is implemented in the back end:

void increment() {
    counter++;
}

When we auto-generate classes, the actual behavior of increment() cannot be auto-generated, because there's no way to tell the autogenerator the kind of behavior this function should have. To define this behavior, you need to implement a complete back end in C++.

The QIfSimulationEngine makes this task more flexible: It utilizes scripting to forward all of the C++ calls to QML, allowing you to use QML to define the behavior. Consequently, you can override the behavior and also script a default behavior without touching any of the C++ code.

For more information on working with the simulation engine, see QIfSimulationEngine.

Separating Data from Logic

The simulation system makes it possible to separate the simulation business logic from the simulation data. The simulation data is stored in JSON files. This is required for the QIfSimulationEngine::loadSimulationData() function to load. Once the simulation data is loaded, the IfSimulator global object provides the content to all QML simulation files. More information about the Data format can be found here

For example, you can use the IfSimulator::findData function to read the data for a specific interface only:

property var settings : IfSimulator.findData(IfSimulator.simulationData, "QIfClimateControl")

Boundary Checks

The IfSimulator global object also provides functions to make boundary checks easier. The property boundaries are defined in the JSON files, while the QML code stays generic, to work with multiple different boundary checks:

function setAirConditioningEnabled(airConditioningEnabled) {
    if (IfSimulator.checkSettings(airConditioningEnabled, settings["airConditioningEnabled"])) {
        console.log("SIMULATION airConditioningEnabled changed to: " + airConditioningEnabled);
        backend.airConditioningEnabled = airConditioningEnabled
    } else {
        setError("SIMULATION changing airConditioningEnabled is not possible: provided: " + airConditioningEnabled + " constraint: " + IfSimulator.constraint_string(settings["airConditioningEnabled"]));
    }
}

Use the IfSimulator::checkSettings() function to check the specified airConditioningEnabled value against the boundaries defined in the JSON file. If the value is within the boundaries, then it is updated; otherwise an error is returned together with the constraint in a human-readable form.

For more information about simulation and the data format, see IfSimulator.

Override Mechanism

For app development or unit testing, it is often useful to trigger a certain behavior in the back end. For example, when implementing message boxes for error recovery, the app developer may need a way to easily trigger this exact error condition. Suppose that the simulation behavior provided by the back-end developer, for this use case, is not sufficient.

In this case, the Qt Interface Framework Simulation System provides an override system to load your own simulation behavior file or data file, via an environment variable.

Each QIfSimulationEngine can have an additional identifier to override the default behavior file or data file, using the following environment variables:

QTIF_SIMULATION_OVERRIDE=<identifier>=<file>[;<identifier>=<file>]
QTIF_SIMULATION_DATA_OVERRIDE=<identifier>=<file>[;<identifier>=<file>]

Integrate with Qt Interface Framework Generator

The simulation system is already integrated into the Qt Interface Framework Generator tools and it is used automatically when generating code with the backend_simulator format.

The auto-generated plugin uses the QFace module name as the QIfSimulationEngine identifier, to allow overriding at runtime.

All boundary annotations defined in config_simulator are then transformed into a JSON file, and embedded as a resource file into the back end.

For each interface, a QML simulation file is created, providing a default implementation to check the boundaries of each property.

Define Your Own Simulation Files

It's not always convenient to use the autogenerated QML simulation files. You can also define your own QML file by using the simulationFile annotation.

Note: If your QFace file provides multiple interfaces, the corresponding simulation file must provide a simulation for all of these interfaces.

To reuse the auto-generated simulation files from an existing interface as a starting point, you can load these auto-generated simulation files using a QML import statement:

import 'qrc:/simulation/'

Afterwards you should be able to load your modified simulation files like a regular QML file.

© 2023 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.