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()