Develop a Parking App

Provides step-by-step instructions on how to develop a parking app for Neptune 3 UI.

Introduction

This tutorial shows you how to build a Parking App step-by-step with some static data to display the number of parking lots available in a particular area.

The tutorial is split into a few chapters:

  1. Design and implement the basic app
  2. Extend the app and integrate it with Qt Application Manager's intent and notification features
  3. Extend the app with a Middleware API and provide some simulation

Chapter1: Design and Implement the App

We start with our Main.qml file, where we import the modules that we need. Apart from Qt Quick, we require some mandatory imports:

  • application.windows - is necessary for the ApplicationCCWindow
  • shared.Sizes - is an attached property that holds some size values for Neptune 3 UI
  • shared.animations - is necessary for some animations in Neptune 3 UI
import application.windows 1.0
import shared.Sizes 1.0
import shared.Style 1.0
import shared.controls 1.0

ApplicationCCWindow {
    id: root

    property bool parkingStarted: false

    Item {
        x: root.exposedRect.x
        y: root.exposedRect.y
        width: root.exposedRect.width
        height: root.exposedRect.height

We use ApplicationCCWindow as the Parking app's root element, because the app is shown in the Center Console. On top of the ApplicationCCWindow, there's an Item that holds the content. When the app is launched, we use some dedicated APIs to a reserve a rectangular area in the Center Console.

The exposedRect property holds the area of the window that is exposed to the user. This is the area that is not occupied by other UI elements.

Once the application has reserved this area, let's start working on the UI.

Set up the UI

There are some properties that you can use from the existing ones attached, as well as Neptune 3 UI animations. Among them are:

PropertyDescription
SizesSizes.dp() is a function to retain the UI pixel density when Neptune 3 UI's windows are being resized. This function converts pixel values from the reference pixel density to the current density. Additionally, Sizes.dp() applies the current scale factor to the given pixel value, effectively converting it into device pixels (dp). Sometimes, this function can also round up the pixels to the nearest integer, to minimize aliasing artifacts.

Note: Some font sizes, such as fontSizeXXS, fontSizeXS, fontSizeS, fontSizeM, fontSizeL, and fontSizeXXL, have predefined values based on Neptune 3 UI's design.

StyleStyle is an attached property that provides values related to the UI style, such as the currently selected theme, colors, and opacity levels.
AnimationsThere are a few default animations and smoothed animations that you can use when you need to apply this behavior to the UI. They hold predefined values to keep a uniform animation for any moving objects in Neptune 3 UI.

Typically, Neptune 3 UI applications are divided into two parts: top content and bottom content. This is the same design philosophy we use for the Music App and Calendar App. Now, for the Parking App:

  • the top content is for the parking ticket
  • the bottom content is to display details
Fill in the Top Content

Since the top content has a background, we use Image as its root and set the background source. To return the correct image source, we use Style.image("app-fullscreen-top-bg", Style.theme). Style.image() is a function that requires an image file name and current selected theme. In Neptune 3 UI, we support two themes: dark (the default) and light. For each asset we use, we have to provide two files; one for each theme.

        Image {
            id: topContent
            width: parent.width
            height: Sizes.dp(500)
            source: Style.image("app-fullscreen-top-bg", Style.theme)

            Label {
                text: qsTr("No active parking tickets")
                anchors.centerIn: parent
                font.weight: Font.Light
                opacity: !root.parkingStarted ? 1.0 : 0.0
                Behavior on opacity { DefaultNumberAnimation {} }
            }

            Image {
                width: root.width * 0.8
                height: topContent.height
                source: "assets/ticket_bg.png"
                anchors.top: parent.top
                anchors.right: parent.right

                anchors.rightMargin: root.parkingStarted ? 0 : - width * 0.85
                Behavior on anchors.rightMargin { DefaultNumberAnimation {} }

                Column {
                    anchors.left: parent.left
                    anchors.leftMargin: Sizes.dp(130)
                    anchors.verticalCenter: parent.verticalCenter
                    spacing: Sizes.dp(80)
                    opacity: root.parkingStarted ? 1.0 : 0.0
                    Behavior on opacity { DefaultNumberAnimation {} }

                    Label {
                        text: qsTr("Zone \nParking Olympia")
                        font.weight: Font.Light
                        color: "black"
                    }

                    Label {
                        text: "1275"
                        opacity: Style.opacityLow
                        font.weight: Font.Bold
                        font.pixelSize: Sizes.fontSizeXXL
                        color: "black"
                    }
                }

                Rectangle {
                    id: ticketContent
                    property date currentTime: new Date()

                    width: parent.width / 2
                    height: Sizes.dp(425)
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.verticalCenterOffset: Sizes.dp(-12)
                    color: Style.accentColor
                    opacity: root.parkingStarted ? 1.0 : 0.0
                    Behavior on opacity { DefaultNumberAnimation {} }

                    onOpacityChanged: {
                        if (opacity === 1.0) {
                            ticketContent.currentTime = new Date()
                        }
                    }

                    Column {
                        anchors.left: parent.left
                        anchors.leftMargin: Sizes.dp(60)
                        anchors.top: parent.top
                        anchors.topMargin: Sizes.dp(80)
                        spacing: Sizes.dp(45)

                        Label {

                            text: "Started: \ntoday " + Qt.formatDateTime(ticketContent.currentTime, "hh:mm")
                            font.weight: Font.Light
                            opacity: Style.opacityHigh
                            color: "black"
                        }

                        Label {
                            text: qsTr("2h, 14 minutes")
                            font.weight: Font.Light
                            opacity: Style.opacityHigh
                            color: "black"
                        }

                        Label {
                            text: "2.29 $"
                            font.weight: Font.Light
                            opacity: Style.opacityHigh
                            color: "black"
                        }
                    }
                }
            }
        }

Then, we add some details to indicate when there's no active parking ticket purchased. If there's an active ticket available, we display that ticket asset. Based on Neptune 3 UI's design, we need to animate the ticket, when it becomes active, by moving it from right to left. This is achieved via the following lines:

anchors.rightMargin: root.parkingStarted ? 0 : - width * 0.85
Behavior on anchors.rightMargin { DefaultNumberAnimation {} }

We use the DefaultNumberAnimation{}, which is a predefined animation to support our requirement. The parkingStarted property is enabled when the parking ticket is active. This property applies the ticket margins to the ticket and its behavior, that we also define in that component.

Fill in the Bottom Content

The bottom content displays details on the parking ticket, such as parking zone, price, location, as well as the start button to start the parking ticket. We use Row and Column component to place all the required labels.

        Item {
            width: parent.width
            height: parent.height - topContent.height
            anchors.top: topContent.bottom

            Row {
                anchors.top: parent.top
                anchors.topMargin: Sizes.dp(60)
                anchors.left: parent.left
                anchors.leftMargin: Sizes.dp(50)
                spacing: Sizes.dp(200)

                Column {
                    spacing: Sizes.dp(50)

                    Label {
                        text: qsTr("Zone")
                        font.weight: Font.Light
                        opacity: Style.opacityMedium
                        font.pixelSize: Sizes.fontSizeL
                    }

                    Row {
                        spacing: Sizes.dp(60)

                        Column {
                            Label {
                                text: qsTr("Every day 12 - 22")
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }

                            Label {
                                text: qsTr("Other times")
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }

                            Label {
                                text: qsTr("Service fee")
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }
                        }

                        Column {
                            Label {
                                text: qsTr("1.5 $ / started hour")
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }

                            Label {
                                text: qsTr("1 $ / started hour")
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }

                            Label {
                                text: "0.29 $"
                                font.weight: Font.Light
                                font.pixelSize: Sizes.fontSizeS
                                opacity: Style.opacityMedium
                            }
                        }
                    }
                }

                Column {
                    spacing: Sizes.dp(250)

                    Label {
                        anchors.right: parent.right
                        text: qsTr("1275, Parking Olympia")
                        font.weight: Font.Light
                        opacity: Style.opacityMedium
                    }

                    Button {
                        id: startButton
                        implicitWidth: Sizes.dp(250)
                        implicitHeight: Sizes.dp(70)
                        font.pixelSize: Sizes.fontSizeM
                        checkable: true
                        checked: root.parkingStarted
                        text: !root.parkingStarted ? qsTr("Start") : qsTr("End (2.29 $)")

                        background: Rectangle {
                            color: {
                                if (startButton.checked) {
                                    return "red";
                                } else {
                                    return "green";
                                }
                            }
                            opacity: {
                                if (startButton.pressed) {
                                    return 0.1;
                                } else if (startButton.checked) {
                                    return 0.3;
                                } else {
                                    return 0.3;
                                }
                            }
                            Behavior on opacity { DefaultNumberAnimation {} }
                            Behavior on color { ColorAnimation { duration: 200 } }

                            radius: width / 2
                        }

                        onClicked: root.parkingStarted = !root.parkingStarted
                    }
                }
            }
        }

In the code snippet above, the start button is an interesting part. Since Neptune 3 UI mostly uses QtQuickControls 2, we can predefine a default style for all of the buttons. However, in this Parking App, we customize our button and use a background that has different colors and behavior.

Note: To view the types of buttons available, run Neptune 3 UI and start the Sheets App.

Add a Manifest File

When we're ready to run the app, we need to add an info.yaml manifest file that contains the lines below:

formatVersion: 1
formatType: am-application
---
id:      'chapter1-basics'
icon:    'icon.png'
code:    'Main.qml'
runtime: 'qml'
name:
  en: 'Parking'

We need to specify an icon for the Parking App, to display in the App Launcher, together with the other apps, once it's installed in the System UI. For more information on info.yaml, see Neptune 3 UI - App Development and Manifest Definition.

Add a Project File

Next, we also need to create a project file, .pro, that speciifies the Parking App project as follows:

TEMPLATE = aux

FILES += info.yaml \
         icon.png \
         Main.qml

assets.files += assets/*
assets.path = $$[QT_INSTALL_EXAMPLES]/neptune3-ui/chapter1-basics/assets

app.files = $$FILES
app.path = $$[QT_INSTALL_EXAMPLES]/neptune3-ui/chapter1-basics

INSTALLS += app assets

AM_MANIFEST = info.yaml
AM_PACKAGE_DIR = $$app.path

load(am-app)

If you use Qt Creator and have the Qt Creator Plugin for Qt Application Manager installed, you can deploy and run your app directly in Neptune 3 UI's System UI. To do that, follow these steps:

  • Open your .pro file in Qt Creator.
  • In the Projects view, under Build & Run, select Run. Verify that your configuration values match the values shown below:

When the project is prepared, press Ctrl+R to run the Parking App in Neptune 3 UI.

Note: Before you can deploy and run the Parking App, make sure that Neptune 3 UI is running.

Chapter 2: Extend the Parking App and Integrate with Intent and Notification

In this chapter, we learn how to extend our Parking App and integrate it with Intent and Notification. Currently, this app shows static data only, and lets you start and stop the parking session with minimal animation.

Integrate Intent from Qt Application Manager

Qt Application Manager makes it possible for an app to talk to another app or to the System UI by sending a signal and then expecting a return value (information) in response.

Suppose we need to be able to make a call to a fictitious Neptune Support Team that manages the Parking Ticket Service. We can add button to make such a call. Remember that in Neptune 3 UI, there is a built-in Phone App. We can send a command to the Phone App and make this call.

Let's start by adding a new Call button:

            Button {
                implicitWidth: Sizes.dp(250)
                implicitHeight: Sizes.dp(70)

                anchors.left: parent.left
                anchors.leftMargin: Sizes.dp(100)
                anchors.top: parent.top
                anchors.topMargin: Sizes.dp(340)

                font.pixelSize: Sizes.fontSizeM
                text: qsTr("Call for support")

                onClicked: sendIntent();

                function sendIntent() {
                    var appId = "com.pelagicore.phone";
                    var request = IntentClient.sendIntentRequest("call-support", appId, {});
                    request.onReplyReceived.connect(function() {
                        if (request.succeeded) {
                            var result = request.result
                            console.log(Logging.apps, "Intent result: " + result.done)
                        } else {
                            console.log(Logging.apps, "Intent request failed: " + request.errorMessage)
                        }
                    });
                }
            }

When this button is clicked, it sends a call-support request to the Phone App and calls the Neptune Support Team. Since we expect to get a reply message, Phone App sends a reply indicating whether the command was received successfully.

For the Phone App to receive the intent request, it needs to have an intent handler available.

Additionally, this "call-support" intent must be registered in the info.yaml file.

formatVersion: 1
formatType: am-package
---
id:      'com.pelagicore.phone'
icon:    'icon.png'
name:
  en: 'Phone'
  de: 'Telefon'
  cs: 'Telefon'
  ru: 'Телефон'
  zh: '电话'
  ja: '電話'
  ko: '전화'

applications:
- id:      'com.pelagicore.phone'
  code:    'Main.qml'
  runtime: 'qml'
  applicationProperties: { private: { squishPort: 7728 } }

intents:
- id: call-support
- id: activate-app

categories: [ 'phone', 'widget' ]

As shown in the above info.yaml file of phone app, the "call-support" is registered. Then you need to add the Intent Handler in its Store.

    readonly property IntentHandler intentHandler: IntentHandler {
        intentIds: ["call-support", "activate-app"]
        onRequestReceived: {
            switch (request.intentId) {
            case "call-support":
                root.startCall("neptunesupport");
                request.sendReply({ "done": true });
                break;
            case "activate-app":
                root.requestRaiseAppReceived()
                request.sendReply({ "done": true })
                break;
            }
        }
    }

The code above runs the startCall() function and calls the Neptune Support Team when it receives the intent from our Parking App. This function also sends a reply indicating whether the requested action is done. Also, make sure you import QtApplicationManager.Application 2.0 to use the IntentHandler.

Create a Notification

Qt Application Manager lets apps create notifications to be sent and shown in the System UI. Usually, System UI has a notification center that stores all notifications that are created. In Neptune 3 UI, there are two kinds of notifications: sticky and non-sticky. When a notification is created, it's shown for a few seconds on top of the UI. If that notification is sticky, it's stored in the notification center afterwards. The user can then decide to keep these notifications or remove each of them.

To create a notification, first, you need to import QtApplicationManager 2.0. Then, you can create a Notification object as part of the Parking App. Suppose you want to inform the user that the parking duration ends in 5 minutes. You can create the Notification object with some information, as follows:

        Notification {
            id: parkingNotification
            summary: qsTr("Your parking period is about to end")
            body: qsTr("Your parking period will be ended in 5 minutes.
                         Please extend your parking ticket or move your car.")
            sticky: true
        }

Once this notification object is created, you need to add a condition for when the parking duration expires after 5 minutes. Since we only have static data for now, you can create a Timer to simulate this behavior.

        Timer {
            interval: 10000; running: root.parkingStarted;
            onTriggered: {
                   root.parkingStarted = false;
                   parkingNotification.show();
            }
        }

When the user presses the Start button, this timer simulates the parking ticket duration. After 10 seconds, the timer is triggered and the notification is shown. It will also reset the parkingStarted property.

Chapter 3: Extend the Parking App with Middleware API and Simulation

In the previous chapters we've already gone through the UI and the components that are necessary to integrate well with Neptune 3 UI.

In this chapter, we learn how to extend the Parking App with a Middleware API and provide a simulation which shows the number of parking lots currently available.

While this chapter does introduce the Middleware integration, how it works, and what needs to be done to package it correctly, a full deep dive is out of scope. For more in-depth details on how to develop Middleware APIs, refer to the Qt IVI Generator Tutorial.

Note: This application requires a multi-process environment.

Define the Middleware API

To define our Middleware API, we use the IVI Generator from the QtIvi module. This generator uses an Interface Definition Language (IDL) to generate code, significantly reducing the amount of code we need to write.

QFace

QtIvi uses the QFace IDL to describe what needs to be generated. For this example, we define a simple interface, ParkingInfo, that provides a readonly property called freeLots, inside a Parking module.

@config_simulator: { simulationFile: "qrc:/simulation.qml" }
module Example.Parking 1.0;

interface ParkingInfo {
    @config_simulator: { default: 42 }
    readonly int freeLots
}
Autogeneration

Now that the first version of our IDL file is ready, it's time to autogenerate API from it with the IVI Generator tool. Similar to moc, this autogeneration process is integrated into the qmake Build System and is done at compile time.

In the following .pro file, we build a C++ library based on our IDL file:

TARGET = $$qtLibraryTarget(Parking)
TEMPLATE = lib
DESTDIR = ..

QT += ivicore ivicore-private qml quick
CONFIG += unversioned_libname unversioned_soname

DEFINES += QT_BUILD_EXAMPLE_PARKING_LIB
CONFIG += ivigenerator
QFACE_SOURCES = ../parking.qface

macos: QMAKE_SONAME_PREFIX = @rpath

target.path = $$[QT_INSTALL_EXAMPLES]/neptune3-ui/chapter3-middleware/
INSTALLS += target

By adding ivigenerator to the CONFIG variable, qmake's IVI Generator integration is loaded and it expects a QFace IDL file in the \C QFACE_SOURCES variable. The set DEFINE makes sure that the library exports its symbols, which is necessary for Windows systems.

Which Files are Autogenerated

The IVI Generator works based on generation templates -- these define what content should be generated from a QFace file. If no QFACE_FORMAT is defined, then the default template, frontend is used. For more details on these templates, see Use the Generator.

This frontend template generates:

  • a C++ class derived from QIviAbstractFeature for every interface in the QFace file
  • one module class that helps to register all interfaces to QML and stores global types and functions

These files are available in your library's build folder, should you wish to inspect the C++ code.

QML Plugin

In addition to the library that contains our Middleware API, we also need a QML plugin to be able to use the API from within QML.

The IVI Generator can help us generate such a plugin with a different generation template. The following .pro file generates a QML plugin that exports the API to QML:

TEMPLATE = lib
CONFIG += plugin
QT += ivicore

LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(Parking)
INCLUDEPATH += $$OUT_PWD/../frontend
QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../../../

QFACE_FORMAT = qmlplugin
QFACE_SOURCES = ../parking.qface

load(ivigenerator)

DESTDIR = $$OUT_PWD/$$replace(URI, \\., /)

exists($$OUT_PWD/qmldir) {
    cpqmldir.files = $$OUT_PWD/qmldir \
                     $$OUT_PWD/plugins.qmltypes
    cpqmldir.path = $$DESTDIR
    cpqmldir.CONFIG = no_check_exist
    COPIES += cpqmldir

    installPath = $$[QT_INSTALL_EXAMPLES]/neptune3-ui/chapter3-middleware/imports/$$replace(URI, \\., /)
    qmldir.files = $$OUT_PWD/qmldir \
                   $$OUT_PWD/plugins.qmltypes
    qmldir.path = $$installPath
    target.path = $$installPath
    INSTALLS += target qmldir
}

We use CONFIG to build a plugin, then define the settings for the linker to link against our frontend library. Next, we use QFACE_FORMAT to choose qmlplugin as the generation template. Instead of adding ivigenerator to the CONFIG, this time we use qmake's load() function to explicitly load the feature. This way, we can use the URI variable, that's part of the qmlplugin generation template. This variable can define a DESTDIR by repliacing all dots with slashes.

In addition to the folder structure, the QmlEngine also needs a qmldir file which indicates what files are part of the plugin, and under which URI. For more information, see Module Definition qmldir Files.

Both -- the qmldir file and the plugins.qmltypes file -- are autogenerated by the IVI Generator and provide information about code-completion; but they need to be placed next to the library. To do so, we add these files to a scope similar to an INSTALL target, but add it to the COPIES variable instead. This makes sure that the files are copied when the plugin is built.

QML Integration

After we've generated our Middleware API and the accompanying QML plugin, it's time to integrate our new API into the Parking App.

For the QML plugin, the module name in our IDL file is used as the import URI; the default import version is 1.0. The import statement for our main.qml file looks like this:

import Example.Parking 1.0

The QML API, by default, uses the same name as the interface in our IDL file. For more information on how to use a custom name or import URI, see Use the Generator.

Our interface can now be instantiated and we set an ID, like with any other QML element:

        ParkingInfo {
            id: parkingInfo
        }

To show the parking lots currently available, we need to create a QML binding using the freeLots property in our newly added ParkingInfo QML element:

                        text: parkingInfo.freeLots + qsTr(", Parking Olympia")
Necessary Adaptations for Packaging

With a normal Qt QML application, these steps would be enough to start the application now and see that the number of free lots is 0, because it's initialized to the default value. But, because we're developing an app for Neptune 3 UI and intend to package it and install it while Neptune 3 UI is running, some additional steps are necessary.

Usually when we build a library, two symbolic links are created to allow for version upgrades without the need to recompile other applications. But in an ApplicationManager package, symbolic links aren't allowed for security reasons. Consequently, the following qmake CONFIG needs to be set to not create those symbolic links; and not sure them when linking to the library:

CONFIG += unversioned_libname unversioned_soname

For our QML plugin to work correctly, we need to set one additional import path to the qmlengine. Usually, this is done using the QML2_IMPORT_PATH environment variable, passing it to the qmlscene or using the QQmlEngine::addImportPath() in your main.cpp. But, because the ApplicationManager starts the app after the installation, and we don't package our own main.cpp file, we need to define those settings in the package manifest, info.yaml. For the import path, we add the following line:

runtimeParameters:
    importPaths: [ 'imports' ]

With those settings in place, the app can be deployed. It should show 0 free parking lots:

Define a Simulation Behavior

To simulate some values for our Middleware API, first we need to understand QtIvi's architecture a little bit better. As we learned when generating the library, the IVI Generator used a template called frontend. To define some simulation values or to connect to a real API, we also need corresponding backend. This backend is provided in the form of a plugin, and QtIvi takes care to load and connect the frontend to the backend. For more information on this concept, see Dynamic Backend Architecture.

Backend Plugin with Static Values

The next step is to generate such a backend using the IVI Generator and use Annotations to define what the simulation should do.

Let's start with the .pro to generate and build our backend:

TEMPLATE = lib
TARGET = $$qtLibraryTarget(parking_simulation)
DESTDIR = ../qtivi

QT += core ivicore
CONFIG += ivigenerator plugin

LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(Parking)
INCLUDEPATH += $$OUT_PWD/../frontend
QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../

QFACE_FORMAT = backend_simulator
QFACE_SOURCES = ../parking.qface
PLUGIN_TYPE = qtivi

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH = $$OUT_PWD/../frontend/qml

target.path = $$[QT_INSTALL_EXAMPLES]/neptune3-ui/chapter3-middleware/qtivi
INSTALLS += target

RESOURCES += \
    simulation.qrc

To build a plugin, we need to add plugin to the CONFIG variable as well as change the QFACE_FORMAT to use the backend_simulator generation template. Similar to the QML plugin, the backend also needs to link to our frontend library, since it uses the types defined there.

TO ensure that QtIvi can find the backend, it needs to be placed in a qtivi folder. In turn, this folder needs to be part of the Qt plugin search path.

Just like with the import path, the additional plugin path needs to be setup in the package manifest:

runtimeParameters:
    pluginPaths: [ '.' ]

Now, we have created a simulation backend, but without additional information, the IVI Generator can't create something really useful.

First, we define a static default value which the simulation backend should provide. The easiest way to is to use an annotation in our QFace IDL file. An annotation is a special type of comment which gives the generation template additional information on what should be generated. To define a default value we change the IDL file like this:

module Example.Parking 1.0;

interface ParkingInfo {
    @config_simulator: { default: 42 }
    readonly int freeLots
}

Because of the change to the IDL file, the IVI Generator now recreates the backend plugin. Now, when we run the updated application, we should see 42 free parking lots.

Simulation QML

While it's useful to have the annotation define a default value and provide a static simulation, having a generated simulation backend can do more. It would also allow you to define a simulation behavior that's more dynamic.

To achieve this, we add another annotation to our QFace IDL file and define a simulationFile. This file contains our simulation behavior and QIviSimulationEngine loads it. Similar to other QML files, the best way to serve this file is to embed it inside a Qt resource.

Our simulation.qml looks like this:

import QtQuick 2.10
import Example.Parking.simulation 1.0

QtObject {
    property var settings : IviSimulator.findData(IviSimulator.simulationData, "ParkingInfo")
    property bool defaultInitialized: false
    property LoggingCategory qLcParkingInfo: LoggingCategory {
        name: "example.parking.simulation.parkinginfobackend"
    }
    property var backend : ParkingInfoBackend {

        function initialize() {
            console.log(qLcParkingInfo, "INITIALIZE")
            if (!defaultInitialized) {
                IviSimulator.initializeDefault(settings, backend)
                defaultInitialized = true
            }
            Base.initialize()
        }

        property var timer: Timer {
            interval: 5000
            running: true
            repeat: true
            onTriggered: {
                var min = Math.ceil(-5)
                var max = Math.floor(5)
                var delta = Math.floor(Math.random() * (max - min +1)) + min;

                var newValue = Math.max(0, backend.freeLots + delta);

                backend.freeLots = newValue;
            }
        }
    }
}

First, there's a settings property, that's initialized with the return value from the IviSimulator.findData method, which takes the IviSimulator.simulationData and a string as input. The simulationData is the JSON file represented as a JavaScript object.

The findData method helps us to extract only data that is of interest for this interface, InstrumentCluster. The properties that follow help the interface to know whether the default values are set. The LoggingCategory is used to identify the log output from this simulation file.

Afterwards, the actual behavior is defined by instantiating an InstrumentClusterBackend Item and extending it with more functions. The InstrumentClusterBackend is the interface towards our InstrumentCluster QML frontend class. But, apart from the frontend, these properties are also writable to make it possible to change them to provide a useful simulation.

Each time a frontend instance connects to a backend, the initialize() function is called. The same applies to the QML simulation: as the initialize() C++ function forwards this to the QML insitance. This behavior also applies to all other functions, like getters and setters. For more details, see QIviSimulationEngine.

Inside the QML initialize() function, we call IviSimulator.initializeDefault(), to read the default values from the simulationData object and iniitialize all properties. This is done only once, as we don't want the properties to be reset to default when the next frontend instance connects to the backend. Lastly, the base implementation is called to make sure that the initializeationDone signal is sent to the frontend.

Next, we define the actual simulation behavior by creating a Timer element that triggers every 5 seconds. In the binding to the trigger signal, we use the Math.random() function to get a random value between -5 and 5 and add this to the parking lots available, using the freeLots property in our backend. The change in this value gets automatically populated to the frontend and simulates a real car park.

Files:

Images:

© 2019 Luxoft Sweden AB. 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.