C
Qt Quick Ultralite Motorcycle Cluster Demo
# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial
cmake_minimum_required(VERSION 3.21.1)
project(motor_cluster VERSION 0.0.1 LANGUAGES C CXX ASM)
if(QUL_PLATFORM MATCHES "^gerda-4")
# Gerda platforms entrypoint function is same as baremetal
set(QUL_OS_LOWERCASE "baremetal")
endif()
find_package(QulApp)
# --- Variant resolution -------------------------------------------------------
# Per-platform QML and image overrides live in "+<variant>/" subdirectories.
# Only one value per axis is ever active, and which one is known at configure
# time, so each axis resolves to a directory segment ("" or "+big/", "" or
# "+intro/") that is concatenated into the file paths below.
# Defaults to the large-screen variant on the only board with a big enough
# panel. Can be overridden on the command line.
set(MC_DEFAULT_SIZE_VARIANT "")
if(QUL_PLATFORM STREQUAL "mimxrt1170-evkb-freertos")
set(MC_DEFAULT_SIZE_VARIANT "big")
endif()
set(SIZE_VARIANT "${MC_DEFAULT_SIZE_VARIANT}" CACHE STRING "Variant for motor cluster demo")
# Large-screen size variant: swaps in "+big/" QML/image overrides.
set(MC_SIZE_DIR "")
if(SIZE_VARIANT STREQUAL "big")
set(MC_SIZE_DIR "+big/")
endif()
# Every variant but the large-screen one stores the fuelGauge and mainScreen
# artwork losslessly compressed. The big variant keeps it uncompressed, since the
# same artwork is redrawn every frame at a larger size there. The startup
# animation frames are compressed in all variants.
set(MC_ARTWORK_LOSSLESS LOSSLESS)
if(SIZE_VARIANT STREQUAL "big")
set(MC_ARTWORK_LOSSLESS "")
endif()
# Startup intro animation: only qt (desktop preview) and stm32f769 enable it.
set(MC_HAS_INTRO FALSE)
set(MC_INTRO_DIR "")
if(QUL_PLATFORM MATCHES "^qt" OR QUL_PLATFORM MATCHES "^stm32f769")
set(MC_HAS_INTRO TRUE)
set(MC_INTRO_DIR "+intro/")
endif()
# stm32f769 plays the startup animation at a reduced frame rate. See the note on
# the loading animation image sequence further down for why.
set(MC_15FPS FALSE)
if(QUL_PLATFORM MATCHES "^stm32f769")
set(MC_15FPS TRUE)
endif()
# The root/entrypoint file only has a dedicated platform-specific override for
# ra8d1; every other platform follows the general size-variant axis.
if(QUL_PLATFORM MATCHES "^ek-ra8d1")
set(MC_MAIN_QML "qmls/main/+ra8d1/motor_cluster.qml")
else()
set(MC_MAIN_QML "qmls/main/${MC_SIZE_DIR}motor_cluster.qml")
endif()
# --- QML files ---------------------------------------------------------------
set(MOTOR_CLUSTER_QML_FILES
${MC_MAIN_QML}
qmls/MotorClusterMain.qml
qmls/Background.qml
qmls/Blinker.qml
qmls/gauges/StartupConfig.qml
qmls/keyboard/KeyFont20.qml
qmls/keyboard/KeyFont36.qml
qmls/keyboard/KeyWithImage.qml
qmls/MainModel.qml
qmls/navigation/Arrow.qml
qmls/navigation/NavigationModel.qml
qmls/performance/full/PerformanceMetricItem.qml
qmls/performance/full/PerformanceMetrics.qml
qmls/RoadView.qml
qmls/Style.qml
qmls/telltales/TellTalesIndicator.qml
qmls/telltales/TellTalesModel.qml
qmls/utils/ClipingItem.qml
qmls/utils/ItemWithAcivationAnimations.qml
qmls/utils/MathAPI.qml
qmls/utils/TextInRoundedBox.qml
# Files with a "+big/" override for the large-screen size variant
qmls/bottomElements/${MC_SIZE_DIR}BottomCommonElements.qml
qmls/bottomElements/${MC_SIZE_DIR}OdoTextElement.qml
qmls/bottomElements/${MC_SIZE_DIR}TextElement.qml
qmls/bottomElements/${MC_SIZE_DIR}TimeView.qml
qmls/gauges/${MC_SIZE_DIR}FuelGaugeView.qml
qmls/gauges/${MC_SIZE_DIR}GearGaugeView.qml
qmls/keyboard/${MC_SIZE_DIR}InputTextField.qml
qmls/keyboard/${MC_SIZE_DIR}KeyBase.qml
qmls/keyboard/${MC_SIZE_DIR}Keyboard.qml
qmls/keyboard/${MC_SIZE_DIR}KeyboardView.qml
qmls/${MC_SIZE_DIR}MotorCluster.qml
qmls/navigation/${MC_SIZE_DIR}Navigation.qml
qmls/${MC_SIZE_DIR}RangeIconWithText.qml
qmls/${MC_SIZE_DIR}SpeedoView.qml
qmls/${MC_SIZE_DIR}Statistics.qml
qmls/tacho/${MC_SIZE_DIR}NumberedScaleView.qml
qmls/tacho/${MC_SIZE_DIR}TachoView.qml
qmls/telltales/${MC_SIZE_DIR}TellTalesView.qml
qmls/${MC_SIZE_DIR}Warning.qml
# Logo has independent size and intro variants; all four combinations
# (base, +big/, +intro/, +big/+intro/) exist as real files, so both
# variables can just be concatenated directly.
qmls/${MC_SIZE_DIR}${MC_INTRO_DIR}Logo.qml
# Startup animation has an intro-specific variant (no size variant)
qmls/animation/${MC_INTRO_DIR}StartupAnimation.qml
)
# StartupAnimationConfig.qml only exists under +intro/ (there is no base
# version at all), with a further +15fps/ variant for stm32f769.
if(MC_HAS_INTRO)
if(MC_15FPS)
list(APPEND MOTOR_CLUSTER_QML_FILES qmls/animation/+intro/+15fps/StartupAnimationConfig.qml)
else()
list(APPEND MOTOR_CLUSTER_QML_FILES qmls/animation/+intro/StartupAnimationConfig.qml)
endif()
endif()
if(QUL_PLATFORM MATCHES "autosar")
qul_add_library(motor_cluster)
else()
qul_add_executable(motor_cluster)
endif()
# simulationcontroller.h is the only header declaring a QML interface. The rest are
# ordinary implementation headers, and do not need to go through qmlinterfacegenerator
set_source_files_properties(
src/mathutils.h
src/statemachine.h
src/simulation/drivetrain.h
src/simulation/normaldrivestate.h
src/simulation/smfwd.h
src/simulation/stateid.h
src/simulation/states.h
PROPERTIES
QUL_NO_QML_INTERFACE ON
)
qul_add_qml_module(motor_cluster
URI MotorClusterApp
QML_FILES ${MOTOR_CLUSTER_QML_FILES}
SOURCES
src/mathutils.h
src/statemachine.h
src/simulation/drivetrain.cpp
src/simulation/drivetrain.h
src/simulation/normaldrivestate.cpp
src/simulation/normaldrivestate.h
src/simulation/simulationcontroller.cpp
src/simulation/simulationcontroller.h
src/simulation/smfwd.h
src/simulation/stateid.h
src/simulation/states.cpp
src/simulation/states.h
)
# --- Image resources -----------------------------------------------------------
#
# All QML files, variant-specific or shared, reference images by a single fixed
# relative path (e.g. "images/mainScreen/road.png"). Each variant image group is
# therefore mapped onto that same fixed resource path via
# qul_add_resources(BASE/PREFIX), whichever "+big/"/"+intro/" directory it is
# actually sourced from.
#
# mc_add_variant_images resolves one such group: FILES are bare filenames
# under images/<DIR>/ (or images/ directly if DIR is empty), read from
# whichever variant directory VARIANT_DIR points to.
function(mc_add_variant_images)
set(options LOSSLESS)
set(oneValueArgs DIR VARIANT_DIR)
set(multiValueArgs FILES)
cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(arg_DIR)
set(source_dir "images/${arg_DIR}/${arg_VARIANT_DIR}")
set(target_prefix "MotorClusterApp/images/${arg_DIR}")
else()
set(source_dir "images/${arg_VARIANT_DIR}")
set(target_prefix "MotorClusterApp/images")
endif()
set(source_files "")
foreach(file_name ${arg_FILES})
list(APPEND source_files "${source_dir}${file_name}")
endforeach()
if(arg_LOSSLESS)
set_source_files_properties(${source_files} PROPERTIES
QUL_RESOURCE_IMAGE_PIXEL_FORMAT AutomaticCompressedLossless
)
endif()
# ra8d1 stores all image resources in external flash rather than the
# default storage section.
if(QUL_PLATFORM MATCHES "^ek-ra8d1")
set_source_files_properties(${source_files} PROPERTIES
QUL_RESOURCE_STORAGE_SECTION "QulResourceDataInExternalFlash"
)
endif()
qul_add_resources(motor_cluster
FILES ${source_files}
BASE "${source_dir}"
PREFIX "${target_prefix}"
)
endfunction()
# The full-screen background and the mainScreen artwork are large and redrawn
# every frame, so on TRAVEO T2G and RA8D1 they are kept out of the resource
# cache. As above, the properties have to be set before mc_add_variant_images.
if(QUL_PLATFORM MATCHES "^tviic2d" OR QUL_PLATFORM MATCHES "^ek-ra8d1")
set(MC_UNCACHED_IMAGES
images/bg/${MC_SIZE_DIR}main.png
images/bg/${MC_SIZE_DIR}main-light.png
)
foreach(mainscreen_file
left-lines-day.png left-lines.png right-lines-day.png right-lines.png
road-day.png road.png tacho-bottom-line-day.png tacho-bottom-line.png
tacho-top-line-day.png tacho-top-line.png)
list(APPEND MC_UNCACHED_IMAGES images/mainScreen/${MC_SIZE_DIR}${mainscreen_file})
endforeach()
set_source_files_properties(${MC_UNCACHED_IMAGES} PROPERTIES
QUL_RESOURCE_CACHE_POLICY NoCaching
)
endif()
mc_add_variant_images(FILES range.png turn-left.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR bg FILES main.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR fuelGauge FILES ring-2.png wave-bottom-part.png wave-shadow.png wave-top-part.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR fuelGauge ${MC_ARTWORK_LOSSLESS} FILES fuel-colored-bg.png fuel-colored-circle-day.png fuel-colored-circle.png fuel-scale.png hole-in-bg-day.png hole-in-bg.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR keyboard FILES backspace-key.png enter-key.png go-home-key.png go-work-key.png minimize-keyboard.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR navigation FILES destination-pin.png tbt-1-arrow-left.png tbt-1-arrow-stright.png tbt-center-separator.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR status FILES battery-status.png distance-status.png engine-oil-status.png fuel-avg-status.png service-status.png time-status.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR tacho FILES 1.png 2.png 3.png 4.png 5.png 6.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR tacho/light FILES 01.png 02.png 03.png 04.png 05.png 06.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR telltales FILES battery.png engine-failure.png engine-oil.png fuel.png high-beams.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR warnings FILES battery-warning.png engine-failure-warning.png engine-oil-warning.png VARIANT_DIR "${MC_SIZE_DIR}")
mc_add_variant_images(DIR mainScreen ${MC_ARTWORK_LOSSLESS}
FILES left-lines-day.png left-lines.png right-lines-day.png right-lines.png road-day.png road.png
tacho-bottom-line-day.png tacho-bottom-line.png tacho-top-line-day.png tacho-top-line.png
VARIANT_DIR "${MC_SIZE_DIR}"
)
# main-light.png has its own default-pixel-format group (rather than joining
# the rest of bg/) because RH850 overrides its pixel format specifically.
# Properties must be set before mc_add_variant_images (i.e. before
# qul_add_resources) is called, since it reads them eagerly rather than
# through generator expressions.
if(QUL_PLATFORM MATCHES "^rh850")
set_source_files_properties(images/bg/${MC_SIZE_DIR}main-light.png PROPERTIES
QUL_RESOURCE_IMAGE_PIXEL_FORMAT RGB565
)
endif()
mc_add_variant_images(DIR bg FILES main-light.png VARIANT_DIR "${MC_SIZE_DIR}")
# The logo image has independent size and intro variants, same as Logo.qml
# (see MOTOR_CLUSTER_QML_FILES above) - all four combinations exist as real
# files under images/.
mc_add_variant_images(FILES qt-logo.png VARIANT_DIR "${MC_SIZE_DIR}${MC_INTRO_DIR}")
# Startup loading animation sequence: only relevant when the intro is enabled,
# with a "+big/" override for the large-screen variant (nested under
# images/loading/, unlike every other group above). stm32f769 takes every other
# frame.
#
# NOTE: The playback time of this sprite animation would be longer than the
# baseline because the project has more image files than the expected
# number of files targeting frame rate (15fps). It is for more smooth
# animation under the limited hardware capacity.
if(MC_HAS_INTRO)
if(SIZE_VARIANT STREQUAL "big")
set(MC_LOADING_VARIANT_DIR "+big/")
else()
set(MC_LOADING_VARIANT_DIR "")
endif()
if(MC_15FPS)
set(MC_LOADING_FRAME_STEP 2)
else()
set(MC_LOADING_FRAME_STEP 1)
endif()
set(MOTOR_CLUSTER_LOADING_FRAMES "")
foreach(frame RANGE 1 89 ${MC_LOADING_FRAME_STEP})
string(LENGTH "${frame}" frame_digits)
math(EXPR frame_pad_len "4 - ${frame_digits}")
string(REPEAT "0" ${frame_pad_len} frame_pad)
list(APPEND MOTOR_CLUSTER_LOADING_FRAMES "${frame_pad}${frame}.png")
endforeach()
# Properties must be set before mc_add_variant_images (i.e. before
# qul_add_resources) is called, since it reads them eagerly rather than
# through generator expressions.
set(MOTOR_CLUSTER_LOADING_SOURCE_FILES "")
foreach(frame_file ${MOTOR_CLUSTER_LOADING_FRAMES})
list(APPEND MOTOR_CLUSTER_LOADING_SOURCE_FILES "images/loading/${MC_LOADING_VARIANT_DIR}${frame_file}")
endforeach()
set_source_files_properties(${MOTOR_CLUSTER_LOADING_SOURCE_FILES} PROPERTIES
QUL_RESOURCE_ANIMATED_SPRITE ON
QUL_RESOURCE_CACHE_POLICY NoCaching
)
mc_add_variant_images(DIR loading LOSSLESS FILES ${MOTOR_CLUSTER_LOADING_FRAMES} VARIANT_DIR "${MC_LOADING_VARIANT_DIR}")
endif()
set_target_properties(motor_cluster PROPERTIES
QUL_FONT_FILES "${CMAKE_CURRENT_SOURCE_DIR}/fonts/Barlow-Medium.ttf;${CMAKE_CURRENT_SOURCE_DIR}/fonts/Barlow-MediumItalic.ttf"
)
target_include_directories(motor_cluster PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
# mimxrt1170-evkb-freertos and ra8d1 run in landscape with the panel mounted
# in portrait orientation, so the framebuffer needs a 90 degree rotation.
if(QUL_PLATFORM STREQUAL "mimxrt1170-evkb-freertos" OR QUL_PLATFORM MATCHES "^ek-ra8d1")
set_target_properties(motor_cluster PROPERTIES
QUL_DISPLAY_ROTATION_ANGLE 90
QUL_SHOW_DESKTOP_PREVIEW_IN_NATURAL_DISPLAY_ORIENTATION false
)
endif()
if(QUL_PLATFORM MATCHES "^ek-ra8d1")
set_target_properties(motor_cluster PROPERTIES
QUL_FONT_FILES_RESOURCE_STORAGE_SECTION "QulFontResourceDataInExternalFlash"
QUL_RESOURCE_CACHE_POLICY "OnStartup"
)
qul_set_maximum_resource_cache_size(motor_cluster CACHE_SIZE 1048576)
endif()
if(QUL_PLATFORM MATCHES "^tviic2d")
target_link_libraries(motor_cluster PRIVATE Qul::PlatformTraveoIIExtras)
if(QUL_OS_LOWERCASE STREQUAL "autosar")
target_sources(motor_cluster PRIVATE src/traveo-t2g/qul_run.cpp)
else()
target_sources(motor_cluster PRIVATE src/traveo-t2g/main.cpp)
endif()
endif()
app_target_setup_os(motor_cluster)
if(NOT QUL_PLATFORM MATCHES "^tviic2d")
app_target_default_entrypoint(motor_cluster motor_cluster MODULE MotorClusterApp)
endif()