C

Qt Quick Ultralite Automotive Cluster Demo

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
import QtQuick 2.15
import QtQuickUltralite.Extras 2.0
import Automotive 1.0
import Benchmark 1.0
import QtQuickUltralite.Profiling

Rectangle {
    id: root
    width: 854
    height: 480

    color: "black"

    Rectangle {
        width: 850
        height: 480
        anchors.centerIn: parent

        color: "#00091a"
        Item
        {
            id: cluster
            anchors.fill: parent
            visible: MainModel.clusterVisible
            opacity: MainModel.clusterOpacity

            Image {
                id: bg
                anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; }
                source: "images/bg-mask.png"

                transform: Scale {
                    origin.x: bg.implicitWidth / 2
                    origin.y: bg.implicitHeight

                    xScale: NormalModeModel.scale
                    yScale: NormalModeModel.scale
                }
            }

            Image {
                id: highlights
                anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; }
                source: "images/car-highlights.png"

                transform: Scale {
                    origin.x: highlights.implicitWidth / 2
                    origin.y: highlights.implicitHeight

                    xScale: NormalModeModel.scale
                    yScale: NormalModeModel.scale
                }
            }

            LaneAssist {
                anchors.fill: parent
                scale: NormalModeModel.scale
            }

            GuideArrowItem {
                anchors.fill: parent
                scale: NormalModeModel.scale
            }

            StaticText {
                id: odo
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 27
                anchors.left: parent.left
                anchors.leftMargin: 30
                text: "ODO"
                color: "#657080"
                font.pixelSize: 12
                font.family: "Sarabun"
            }
            Text {
                id: odoValue
                anchors.baseline: odo.baseline
                anchors.left: odo.right
                anchors.leftMargin: 4
                text: Units.toInt(Units.kilometersToLongDistanceUnit(MainModel.odo))
                color: Style.lightPeriwinkle
                font.pixelSize: 20
                font.family: "Sarabun"
            }
            StaticText {
                id: odoUnit
                anchors.baseline: odo.baseline
                anchors.left: odoValue.right
                anchors.leftMargin: 4
                text: Units.longDistanceUnit
                color: "#657080"
                font.pixelSize: 12
                font.family: "Sarabun"
            }

            StaticText {
                id: range
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 27
                x: 170
                text: "RANGE"
                color: "#657080"
                font.pixelSize: 12
                font.family: "Sarabun"
            }
            Text {
                id: rangeValue
                anchors.baseline: range.baseline
                anchors.left: range.right
                anchors.leftMargin: 4
                text: Units.toInt(Units.kilometersToLongDistanceUnit(MainModel.range))
                color: Style.lightPeriwinkle
                font.pixelSize: 20
                font.family: "Sarabun"
            }
            StaticText {
                id: rangeUnit
                anchors.baseline: range.baseline
                anchors.left: rangeValue.right
                anchors.leftMargin: 4
                text: Units.longDistanceUnit
                color: "#657080"
                font.pixelSize: 12
                font.family: "Sarabun"
            }

            LinearGauge {
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 27
                x: 534
                image: "images/fuel.png"
                emptyText: "R"
                value: MainModel.fuelLevel
            }

            LinearGauge {
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 27
                x: 660
                image: "images/battery.png"
                emptyText: "E"
                value: MainModel.batteryLevel
            }

            Loader {
                id: modeLoader
                anchors.fill: parent
                source: "../NormalMode.qml"
            }

            Connections {
                target: MainModel
                function onClusterModeChanged(clusterMode: int) {
                    changeMode.start()
                }
            }

            Timer {
                id: changeMode
                interval: 600
                onTriggered: {
                    switch(MainModel.clusterMode) {
                        case MainModel.ModeNormal:
                            modeLoader.source = "../NormalMode.qml"
                            break;
                        case MainModel.ModeSport:
                            modeLoader.source = "../SportMode.qml"
                            break;
                    }
                }
            }

            Behavior on opacity { NumberAnimation { duration: MainModel.clusterOpacityChangeDuration; } }
        }

        TellTales {
            anchors.horizontalCenter: parent.horizontalCenter
            y:16
            visible: MainModel.telltalesVisible
        }

        BenchmarkMode {
            id: benchmarkMode
        }

        QulPerfOverlay {
            id: benchmarkResult
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.verticalCenter: parent.verticalCenter
            visible: false
        }

        Timer {
            id: benchmarkTimer
            interval: 30000
            running: benchmarkMode.enabled
            repeat: false
            onTriggered: {
                QulPerf.recording = false
                benchmarkResult.visible = true
                MainModel.clusterVisible = false
                MainModel.telltalesVisible = false
            }
        }

        SimulationController {
            id: simulationController
        }

        Timer {
            interval: 50
            running: true
            repeat: true
            onTriggered: {
                ConnectivityService.sendHeartBeat()
            }
        }

        Timer {
            id: inactivityTimer
            interval: 10000
            running: false
            repeat: false
            onTriggered: {
                simulationController.stopInteractiveMode()
            }
        }

        Timer {
            id: travelTimeCounter
            running: true
            repeat: true
            interval: 1000
            onTriggered: { MainModel.travelTime += 1 }
        }

        function enterInteractiveMode() {
            simulationController.startInteractiveMode();
            if(!MainModel.forceInteractiveMode) {
                inactivityTimer.restart()
            }
        }

        function onHmiInputPressed(key : int) {
            if (!MainModel.introSequenceCompleted) {
                return
            }
            enterInteractiveMode()
        }
        function onHmiInputReleased(key : int) {
            if (!MainModel.introSequenceCompleted) {
                return
            }
            enterInteractiveMode();
            if (MainModel.clusterMode == MainModel.ModeNormal) {
                if (key == HMIInputEvent.HMI_KNOB_TURN_LEFT) {
                    NormalModeModel.previousMenu()
                }
                else if (key == HMIInputEvent.HMI_KNOB_TURN_RIGHT) {
                    NormalModeModel.nextMenu()
                }
                else if (NormalModeModel.menu == NormalModeModel.MediaPlayerMenu) {
                    if (key == HMIInputEvent.HMI_BTN_LEFT) {
                        MediaPlayerModel.previousSong()
                    }
                    else if (key == HMIInputEvent.HMI_BTN_RIGHT) {
                        MediaPlayerModel.nextSong()
                    }
                    else if (key == HMIInputEvent.HMI_KNOB_CENTER) {
                        MediaPlayerModel.mediaPlayback = !MediaPlayerModel.mediaPlayback
                    }
                }
                else if (NormalModeModel.menu == NormalModeModel.PhoneMenu) {
                    if (!PhoneModel.inCall) {
                        if (key == HMIInputEvent.HMI_BTN_LEFT) {
                            PhoneModel.previousTab()
                        }
                        else if (key == HMIInputEvent.HMI_BTN_RIGHT) {
                            PhoneModel.nextTab();
                        }
                        else if (key == HMIInputEvent.HMI_BTN_UP) {
                            PhoneModel.previousContact()
                        }
                        else if (key == HMIInputEvent.HMI_BTN_DOWN) {
                            PhoneModel.nextContact()
                        }
                    }

                    if (key == HMIInputEvent.HMI_KNOB_CENTER) {
                        PhoneModel.inCall = !PhoneModel.inCall
                    }
                }
                else if (NormalModeModel.menu == NormalModeModel.CarStatusMenu) {
                    if (key == HMIInputEvent.HMI_BTN_LEFT) {
                        SettingsMenuModel.previousTab()
                    }
                    else if (key == HMIInputEvent.HMI_BTN_RIGHT) {
                        SettingsMenuModel.nextTab();
                    }

                }
            }

            if ((MainModel.clusterMode == MainModel.ModeNormal && NormalModeModel.menu == NormalModeModel.CarStatusMenu && SettingsMenuModel.currentTab == SettingsMenuModel.DriveModeTab) ||
                (MainModel.clusterMode == MainModel.ModeSport && SportModeModel.menuActive)) {
                if (key == HMIInputEvent.HMI_KNOB_CENTER) {
                    if (SettingsMenuModel.currentDriveModeSelected == SettingsMenuModel.NormalDrive) {
                        simulationController.switchToNormalMode()
                    }
                    else {
                        simulationController.switchToSportMode()
                    }
                }
                else if (key == HMIInputEvent.HMI_BTN_UP || key == HMIInputEvent.HMI_BTN_DOWN) {
                    SettingsMenuModel.switchOption()
                }
            }
            if (MainModel.clusterMode == MainModel.ModeSport && key == HMIInputEvent.HMI_KNOB_TURN_RIGHT) {
                SportModeModel.menuActive = !SportModeModel.menuActive
            }
        }

        function keyToHMI(key : int) : int {
            switch (key) {
                case Qt.Key_Up:       return HMIInputEvent.HMI_BTN_UP
                case Qt.Key_Down:     return HMIInputEvent.HMI_BTN_DOWN
                case Qt.Key_Left:     return HMIInputEvent.HMI_BTN_LEFT
                case Qt.Key_Right:    return HMIInputEvent.HMI_BTN_RIGHT
                case Qt.Key_Space:    return HMIInputEvent.HMI_BTN_CENTER
                case Qt.Key_PageDown: return HMIInputEvent.HMI_KNOB_TURN_LEFT
                case Qt.Key_PageUp:   return HMIInputEvent.HMI_KNOB_TURN_RIGHT
                case Qt.Key_Return:   return HMIInputEvent.HMI_KNOB_CENTER

                default: return HMIInputEvent.HMI_UNKNOWN
            }
        }

        Keys.onPressed: { onHmiInputPressed(keyToHMI(event.key)) }
        Keys.onReleased: { onHmiInputReleased(keyToHMI(event.key)) }

        HMIInput.onPressed: { onHmiInputPressed(key) }
        HMIInput.onReleased: { onHmiInputReleased(key) }

        MainModel.onForceInteractiveModeChanged: {
            if (!MainModel.introSequenceCompleted) {
                return
            }
            if(MainModel.forceInteractiveMode) {
                simulationController.startInteractiveMode()
            }
            else {
                simulationController.stopInteractiveMode()
            }
        }

        Component.onCompleted: {
            simulationController.start()
            MathAPI.benchmarkMode = benchmarkMode.enabled
            if(benchmarkMode.enabled) {
                QulPerf.recording = true
            }
        }
    }
}