Skip to content

feat: runtime screen rotation for the color UI (opt-in) - #359

Open
attakygit wants to merge 7 commits into
meshtastic:masterfrom
Attaky-Module:feat/runtime-screen-rotation
Open

feat: runtime screen rotation for the color UI (opt-in)#359
attakygit wants to merge 7 commits into
meshtastic:masterfrom
Attaky-Module:feat/runtime-screen-rotation

Conversation

@attakygit

@attakygit attakygit commented Jul 31, 2026

Copy link
Copy Markdown

Draft: depends on meshtastic/protobufs#1026, which adds
DeviceUIConfig.screen_rotation. Until that merges and a tag is cut, the
feature-enabled CI job cannot compile — that failure is expected and is the
reason this is a draft. Everything with the feature off is complete and
verified.

What this does

Lets the color UI switch between the landscape and portrait layouts at runtime,
as a settings entry, instead of committing to one at build time via
VIEW_320x240 / VIEW_240x320.

Off by default. MUI_RUNTIME_ROTATION is opt-in; a build without it is
unchanged (evidence below).

Implements the runtime half of what #288 asked for. #294 solved it by adding the
perpendicular layout; this makes choosing between them a user setting.

How

Both generated layouts are linked into one binary. The primary tree (the build's
VIEW_*) keeps its symbols and supplies every shared asset; the secondary tree
is compiled with its colliding globals renamed through a generated forced
include, and a generated bridge republishes its widget pointers into the symbols
the C++ view addresses.

tools/gen_dual_ui.py derives that glue from the trees themselves and asserts
what it depends on: identical objects_t member sets, symmetric global symbols,
byte-identical shared assets, identical non-asset source inventory. --check
regenerates every artifact and byte-compares it, and rejects unexpected files, so
the committed glue cannot drift from the trees unnoticed — it runs on every
build and in CI.

The stored value is a quarter-turn count composed with each board's existing
LGFX_ROTATION offset, so value 0 reproduces the current behaviour on any board.
Even values keep the compile-time layout, odd values select the perpendicular
one. Persistence uses the existing storeUIConfig path, followed by a reboot
request, since the value is only read at boot.

Cost, measured: +66.7 KB flash (1.04 % of a 6.25 MB app partition), +4.5 KB
static RAM. The 93 byte-identical asset translation units compile once, which is
why this is far cheaper than linking two full trees.

Scope limits, stated up front

  • Only VIEW_320x240 and VIEW_240x320. They are the only perpendicular
    pair; any other VIEW_* fails the build with a message saying so.
  • CUSTOM_TOUCH_DRIVER boards are excluded at compile time. Those drivers
    apply a fixed rotation to touch coordinates and would desynchronize from a
    rotated panel. That unfortunately includes the Heltec V4 of [Feature Request]: Custom / rotated UI for Heltec V4 Expansion Kit #288. Making it
    work needs a rotation-aware custom-touch contract, which I left out of this PR
    deliberately — happy to follow up if you want it.

Two independent fixes, first in the series

These stand alone, apply with the feature off, and can be taken without the rest:

  • fix(graphics): the statistics table uses a fixed 36 px column width unless
    the horizontal resolution exceeds 320, so a 240 px screen asks for
    57 + 6*36 = 273 px and overflows. Affects existing compile-time
    VIEW_240x320 builds too.
  • fix(build): set(GENERATED_VIEW ${VIEW} "ui_320x240") produces a
    two-element list as soon as VIEW is set, and VIEW_320x240 was defined
    unconditionally — so the CMake build could only ever produce ui_320x240.
    No change when VIEW is unset.

Verification

Hardware — all four rotations, on device. ESP32-S3 with an ST7789 320x240
panel. Each value was driven through the on-device settings entry, so the whole
chain is covered: store, reboot request, and the value being applied on the next
boot. (Attaky Mesh Series)

value label LVGL panel internal observed
0 Landscape 320x240 3 upright landscape
1 Portrait 240x320 2 upright portrait
2 Landscape flipped 320x240 1 inverted landscape
3 Portrait flipped 240x320 0 inverted portrait

This caught a real bug that was not derivable on paper: quarter turns had to run
opposite to the panel's own numbering, because upright portrait is one step
back from upright landscape on this panel, not one step on.

Feature-off equivalence. Built the branch with the macro undefined and the
same branch minus the feature commits, through the same PlatformIO environment
sequentially — necessary because LTO stamps a per-TU hash derived from the source
path, so two differently-named environments differ everywhere for no reason.

Result: identical length; after masking the ELF-hash at 0xB0..0xCF and the
33-byte trailing digest, 2 bytes differ. Both come from
source/graphics/ScreenRotation.cpp existing at all — with the macro off it
compiles to a genuinely empty TU (size reports text/data/bss all 0), but one
extra TU perturbs LTO's internal numbering. The decisive evidence is
symbol-level: the same 20,934 defined symbols, zero additions or removals,
with text and bss identical.

Which hardware I do not have. Everything above is on one ESP32-S3 + ST7789
board. I have no Heltec V3, T-Deck, T-Beam, RAK4631 or T-1000E to test against,
per the template. The equivalence result above is the reason I think that is
acceptable here: with MUI_RUNTIME_ROTATION undefined those builds get the same
binary they get today, so there is nothing for the feature to regress. Testing
is only needed for boards that actually turn it on.

Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious
    regressions on the listed devices — I do not have them; see above for why
    feature-off builds are unaffected.
    • Other: ESP32-S3 + ST7789 320x240, all four rotations (Attaky Mesh Series)
screen.orintation.-.small.mp4

Summary by CodeRabbit

  • New Features

    • Added runtime screen rotation for supported displays, including portrait and landscape orientations.
    • Added orientation controls in display settings with options for 0°, 90°, 180°, and 270°.
    • Display dimensions and interface layouts now adjust automatically after rotation.
    • Rotation changes are saved and applied after restarting the device.
  • Bug Fixes

    • Improved layout handling for narrow portrait displays to prevent content overflow.
  • Localization

    • Added orientation and restart-related translation entries across supported languages.

The six data columns use a fixed 36 px width unless the horizontal resolution
exceeds 320, so a 240 px screen asks for 57 + 6*36 = 273 px and the table
overflows its panel. Derive the column width from the available space instead.
set(GENERATED_VIEW ${VIEW} "ui_320x240") produces a two-element list as soon
as VIEW is set, and VIEW_320x240 was defined unconditionally, so the CMake
build could only ever produce ui_320x240. Give VIEW a default and derive the
macro from it; behaviour is unchanged when VIEW is unset.
Linking both generated UI trees into one binary requires renaming the secondary
tree's 198 colliding globals and republishing its widget pointers into the
symbols the C++ view addresses. tools/gen_dual_ui.py derives that glue from the
trees themselves and asserts the invariants it depends on: identical objects_t
member sets, symmetric global symbols, byte-identical shared assets, and an
identical non-asset source inventory.

The output is orientation-neutral, so one set of artifacts serves either tree
as the primary one. --check regenerates every artifact and byte-compares it,
and rejects unexpected files, so the committed glue cannot drift from the trees
unnoticed.
The stored value is the quarter-turn count applied on top of the board's
compile-time LGFX_ROTATION offset, so value 0 reproduces each board's current
behaviour. Even values keep the compile-time layout, odd values need the
perpendicular one.

Applied at boot only: an LVGL layout tree cannot be rebuilt in place once the
view has cached its object pointers. The mapping helpers are pure so they can be
tested without touching the loaded state.
Off by default. When enabled, the primary tree supplies every shared asset and
the secondary tree is compiled with the generated rename header forced in, so
both layouts are linked exactly once. Both build systems read the source
inventory from the generator's manifest rather than globbing, and both run
--check so stale glue fails the build.

The CMake path matters because that is what CI builds: without it the feature
would be invisible to CI and effectively untested. X11 is not LGFX, so the new
job covers compiling, linking and the state tests, not the panel rotation
itself. The link test references a symbol from each tree because a static
library would otherwise link green without ever pulling in the secondary one.
The panel is rotated before any layout exists and LVGL is then told the rotated
panel's own resolution, rather than the layout's nominal size: substituting the
latter would misreport panels larger than the layout, such as 320x480 ones.

The view dispatches the secondary tree's ui_init* and publishes the bridge
before any objects.* dereference and before apply_hotfix caches pointers.
Adds a settings entry and a dropdown dialog copied from the generated
dropdown-panel idiom. The option order is the stored value order, and the labels
follow from which layout the build compiled as primary, so an even value always
reads as the built-in aspect.

The choice is persisted through the existing storeUIConfig path and followed by
a reboot request: the value is only read at boot, and the firmware keeps serving
the pre-change config until it reloads. A copy is submitted so a failed store
cannot leave the new value in memory for an unrelated save to persist, and the
dialog stays open unless both submissions succeed. Comparing against the
persisted value rather than the active one keeps a saved-but-not-yet-applied
change retryable.
@CLAassistant

CLAassistant commented Jul 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds dual-layout generation and build support for runtime screen rotation. It adds rotation state management, display integration, orientation controls, localized messages, CI coverage, and tests for layout selection and dimension handling.

Changes

Runtime UI rotation

Layer / File(s) Summary
Dual UI artifact generation
tools/gen_dual_ui.py
The generator validates UI trees, symbols, and assets. It creates rename headers, bridge sources, style-routing headers, manifests, fingerprints, and check-mode validation.
Build and CI integration
CMakeLists.txt, cmake/MuiRuntimeRotation.cmake, extra_script.py, .github/workflows/ci.yml
CMake and PlatformIO add runtime-rotation options, generated sources, include paths, symbol-routing flags, and validation. CI builds and tests a separate runtime-rotation configuration.
Rotation state and display integration
include/graphics/ScreenRotation.h, source/graphics/ScreenRotation.cpp, include/graphics/driver/LGFXDriver.h, include/graphics/view/TFT/TFTView_320x240.h, source/graphics/TFT/TFTView_320x240.cpp
The display selects rotation-specific dimensions, panel orientation, and primary or secondary UI trees. The TFT view declares rotation settings and supports narrower portrait layouts.
Orientation controls and validation
locale/*.yml, source/graphics/TFT/TFTView_320x240.cpp, tests/test_ScreenRotation.cpp
The UI adds orientation selection, persistence, reboot handling, status messages, and localized keys. Tests cover linking, panel mapping, tree selection, dimensions, and invalid stored values.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: mverch67

Poem

A rabbit turns the screen with care,
From landscape hops to portrait air.
Two UI trees now share the stage,
Glue and tests turn every page.
“Restart,” I whisper, “then we spin!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the opt-in runtime screen rotation feature added for the color UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
cmake/MuiRuntimeRotation.cmake (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the primary-tree glob to source files.

file(GLOB_RECURSE ... /generated/${_primary}/*) matches every file in the tree, including headers and any non-source file the UI generator writes. The manifest-driven secondary list uses explicit names, so the two sides are inconsistent. Match *.c explicitly, and add CONFIGURE_DEPENDS so a regenerated tree retriggers configure.

♻️ Proposed fix
-    file(GLOB_RECURSE _primary_sources ${CMAKE_CURRENT_SOURCE_DIR}/generated/${_primary}/*)
+    file(GLOB_RECURSE _primary_sources CONFIGURE_DEPENDS
+         ${CMAKE_CURRENT_SOURCE_DIR}/generated/${_primary}/*.c)
     list(APPEND _sources ${_primary_sources})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/MuiRuntimeRotation.cmake` around lines 59 - 60, Update the
primary-source glob in the CMake logic around _primary_sources to match only
generated C files using an explicit *.c pattern, and add CONFIGURE_DEPENDS so
regenerated files retrigger configuration. Leave the secondary manifest-driven
source handling unchanged.
tools/gen_dual_ui.py (2)

388-391: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Write the artifacts with an explicit encoding.

read opens files with encoding="utf-8", but the write path uses the platform default encoding. On a machine with a non-UTF-8 default locale, the generated bytes and the --check comparison can disagree. Pin the encoding and the newline translation so the committed output is reproducible on every platform.

♻️ Proposed fix
     for name, content in sorted(artifacts.items()):
-        with open(os.path.join(OUT, name), "w") as f:
+        with open(os.path.join(OUT, name), "w", encoding="utf-8", newline="\n") as f:
             f.write(content)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gen_dual_ui.py` around lines 388 - 391, Update the artifact-writing
loop in the generator to open each output file with explicit UTF-8 encoding and
disabled newline translation, matching the read path so generated output is
reproducible across platforms.

120-174: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report stale SECONDARY_ONLY_ASSETS entries.

asset_inventory never validates that each name in SECONDARY_ONLY_ASSETS still exists as a unique asset. If the UI generator removes such a file, or starts emitting it in both trees, the entry becomes dead and the fingerprint silently changes meaning. A short assertion keeps the allowlist honest.

♻️ Proposed check
     listed = {
         v: sorted(n for n in unique[v] if n in SECONDARY_ONLY_ASSETS) for v in VIEWS
     }
+    covered = {n for v in VIEWS for n in listed[v]}
+    stale = sorted(set(SECONDARY_ONLY_ASSETS) - covered)
+    if stale:
+        sys.exit(
+            "ERROR: SECONDARY_ONLY_ASSETS lists asset(s) that are no longer unique to one "
+            "tree: %s; update the list" % ", ".join(stale)
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gen_dual_ui.py` around lines 120 - 174, Update asset_inventory to
validate SECONDARY_ONLY_ASSETS after computing unique, ensuring every
allowlisted name exists as a unique asset in the corresponding view and is not
shared or absent. Fail with a clear error identifying any stale view/name entry
before building listed or updating the fingerprint.
.github/workflows/ci.yml (1)

78-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a matrix instead of duplicated steps.

The three new steps repeat the configure, build, and test flow with one option changed. A job matrix over the MUI_RUNTIME_ROTATION value runs both configurations in parallel and removes the duplication, which keeps total CI wall time close to the current single-configuration time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 78 - 92, Replace the duplicated
runtime-rotation configure, build, and test steps in the CI job with a matrix
over the MUI_RUNTIME_ROTATION setting, covering both disabled and enabled
values. Use the matrix value in the CMake configuration and ensure each matrix
variant builds and runs its tests in a distinct build directory.
extra_script.py (1)

88-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Style routing depends on unverified basenames.

STYLE_ROUTED_CXX matches only the file basename, and cmake/MuiRuntimeRotation.cmake lines 90-94 hardcodes the same two paths. If either file is renamed or moved, both build paths silently stop force-including style_routing.h, and the secondary layout renders unthemed at runtime instead of failing the build. Track whether each expected translation unit was matched, and fail the build when one is missing.

♻️ Proposed guard
     routing_flags = ["-include", join(generated, "style_routing.h")]
     STYLE_ROUTED_CXX = ("Themes.cpp", "TFTView_320x240.cpp")
+    for _name in STYLE_ROUTED_CXX:
+        if not os.path.exists(join(lib_root, "source", "graphics", "TFT", _name)):
+            raise Exception("MUI_RUNTIME_ROTATION: style-routed source %s not found; "
+                            "update STYLE_ROUTED_CXX" % _name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extra_script.py` around lines 88 - 102, Update apply_dual_ui_flags and the
associated style-routing configuration to track whether each expected
translation unit in STYLE_ROUTED_CXX was matched, including the corresponding
CMake paths, and fail the build when either expected file is absent or renamed.
Preserve the existing routing_flags application for valid matches while
preventing a silent unthemed secondary build.
CMakeLists.txt (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validate VIEW before deriving the macro.

Line 38 derives the compile definition by stripping a leading ui_. If a user passes -DVIEW=320x240 or any value without the prefix, the regex does not match and add_compile_definitions receives the raw value, so no VIEW_* macro is defined and the failure appears later as missing UI symbols. A guard turns that into a clear configure-time error.

♻️ Proposed guard
 add_compile_definitions(ARCH_PORTDUINO)
 add_compile_definitions(ARDUINO)
+if(NOT GENERATED_VIEW MATCHES "^ui_[0-9]+x[0-9]+$")
+    message(FATAL_ERROR "VIEW must name a generated view, e.g. ui_320x240 (got '${GENERATED_VIEW}')")
+endif()
 string(REGEX REPLACE "^ui_" "VIEW_" GENERATED_VIEW_MACRO ${GENERATED_VIEW})
 add_compile_definitions(${GENERATED_VIEW_MACRO})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CMakeLists.txt` around lines 28 - 39, Validate the VIEW value after its
default is assigned and before deriving GENERATED_VIEW_MACRO, requiring the
expected ui_ prefix. Emit a clear configure-time error and stop configuration
for invalid values, while preserving the existing regex conversion and
compile-definition behavior for valid views.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmake/MuiRuntimeRotation.cmake`:
- Around line 84-94: Update the project’s cmake_minimum_required declaration in
CMakeLists.txt from version 3.15 to 3.18 so the DIRECTORY option used by
set_source_files_properties in MuiRuntimeRotation.cmake is supported. Preserve
the existing minimum-version configuration otherwise.

In `@locale/cs.yml`:
- Around line 147-154: Replace the seven ~ values for Orientation, Landscape,
Portrait, Landscape flipped, Portrait flipped, Restarting to apply, Save failed,
and Restart required with localized translations in locale/cs.yml lines 147-154,
locale/da.yml lines 168-175, locale/de.yml lines 157-164, locale/el.yml lines
170-177, locale/es.yml lines 165-172, locale/fi.yml lines 153-160, locale/fr.yml
lines 160-167, and locale/hu.yml lines 170-177; preserve the existing keys and
YAML structure.

In `@tests/test_ScreenRotation.cpp`:
- Around line 15-19: Update the “both generated layouts are linked” test to
retain and observe mui_bridge_publish_secondary through a volatile
function-pointer variable or an equivalent non-optimizable link anchor,
replacing the ineffective direct non-null address check while preserving the
existing objects linkage assertion.

In `@tools/gen_dual_ui.py`:
- Around line 349-359: Update main to validate all arguments beginning with "--"
before selecting the view, accepting only "--check" and rejecting unknown flags
with an error exit. Preserve the existing positional view handling and
check_only behavior for valid arguments.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 78-92: Replace the duplicated runtime-rotation configure, build,
and test steps in the CI job with a matrix over the MUI_RUNTIME_ROTATION
setting, covering both disabled and enabled values. Use the matrix value in the
CMake configuration and ensure each matrix variant builds and runs its tests in
a distinct build directory.

In `@cmake/MuiRuntimeRotation.cmake`:
- Around line 59-60: Update the primary-source glob in the CMake logic around
_primary_sources to match only generated C files using an explicit *.c pattern,
and add CONFIGURE_DEPENDS so regenerated files retrigger configuration. Leave
the secondary manifest-driven source handling unchanged.

In `@CMakeLists.txt`:
- Around line 28-39: Validate the VIEW value after its default is assigned and
before deriving GENERATED_VIEW_MACRO, requiring the expected ui_ prefix. Emit a
clear configure-time error and stop configuration for invalid values, while
preserving the existing regex conversion and compile-definition behavior for
valid views.

In `@extra_script.py`:
- Around line 88-102: Update apply_dual_ui_flags and the associated
style-routing configuration to track whether each expected translation unit in
STYLE_ROUTED_CXX was matched, including the corresponding CMake paths, and fail
the build when either expected file is absent or renamed. Preserve the existing
routing_flags application for valid matches while preventing a silent unthemed
secondary build.

In `@tools/gen_dual_ui.py`:
- Around line 388-391: Update the artifact-writing loop in the generator to open
each output file with explicit UTF-8 encoding and disabled newline translation,
matching the read path so generated output is reproducible across platforms.
- Around line 120-174: Update asset_inventory to validate SECONDARY_ONLY_ASSETS
after computing unique, ensuring every allowlisted name exists as a unique asset
in the corresponding view and is not shared or absent. Fail with a clear error
identifying any stale view/name entry before building listed or updating the
fingerprint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a38695f0-a239-40f3-924d-12da30b32f6b

📥 Commits

Reviewing files that changed from the base of the PR and between 85bd4ff and 35131fa.

⛔ Files ignored due to path filters (5)
  • tools/generated/dual/bridge_primary.c is excluded by !**/generated/**
  • tools/generated/dual/bridge_secondary.c is excluded by !**/generated/**
  • tools/generated/dual/manifest.txt is excluded by !**/generated/**
  • tools/generated/dual/style_routing.h is excluded by !**/generated/**
  • tools/generated/dual/ui_secondary_rename.h is excluded by !**/generated/**
📒 Files selected for processing (34)
  • .github/workflows/ci.yml
  • CMakeLists.txt
  • cmake/MuiRuntimeRotation.cmake
  • extra_script.py
  • include/graphics/ScreenRotation.h
  • include/graphics/driver/LGFXDriver.h
  • include/graphics/view/TFT/TFTView_320x240.h
  • locale/bg.yml
  • locale/cs.yml
  • locale/da.yml
  • locale/de.yml
  • locale/el.yml
  • locale/en.yml
  • locale/es.yml
  • locale/fi.yml
  • locale/fr.yml
  • locale/hu.yml
  • locale/it.yml
  • locale/nl.yml
  • locale/no.yml
  • locale/pl.yml
  • locale/pt.yml
  • locale/ro.yml
  • locale/ru.yml
  • locale/se.yml
  • locale/sl.yml
  • locale/sr.yml
  • locale/tr.yml
  • locale/uk.yml
  • locale/zh-CN.yml
  • source/graphics/ScreenRotation.cpp
  • source/graphics/TFT/TFTView_320x240.cpp
  • tests/test_ScreenRotation.cpp
  • tools/gen_dual_ui.py

Comment on lines +84 to +94
set_source_files_properties(
${_secondary_sources} ${MUI_GLUE_DIR}/bridge_secondary.c
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
PROPERTIES COMPILE_OPTIONS "${_rename_flags}")

# C++ sources referencing generated styles dispatch them to the active tree.
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/source/graphics/TFT/Themes.cpp
${CMAKE_CURRENT_SOURCE_DIR}/source/graphics/TFT/TFTView_320x240.cpp
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
PROPERTIES COMPILE_OPTIONS "-include;${MUI_GLUE_DIR}/style_routing.h")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the declared minimum CMake version.
set -euo pipefail
rg -n 'cmake_minimum_required' --glob '*.txt' --glob '*.cmake'

Repository: meshtastic/device-ui

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "CMake files:"
git ls-files | rg '(^|/)(CMakeLists\.txt|.*\.cmake)$' || true

echo
echo "cmake_minimum_required occurrences:"
rg -n --fixed-strings 'cmake_minimum_required' . --glob '*.txt' --glob '*.cmake' || true

echo
echo "Directory option usages:"
rg -n 'set_source_files_properties' --glob '*.cmake' --glob '*.txt' . || true

echo
echo "Relevant file excerpt:"
if [ -f cmake/MuiRuntimeRotation.cmake ]; then
  sed -n '1,130p' cmake/MuiRuntimeRotation.cmake | cat -n
fi

Repository: meshtastic/device-ui

Length of output: 5946


🌐 Web query:

CMake set_source_files_properties DIRECTORY option version documentation

💡 Result:

The DIRECTORY option for the set_source_files_properties command in CMake was introduced in version 3.18 [1][2]. It allows you to set source file properties in specific directory scopes beyond the current CMakeLists.txt file [1][3]. By default, source file properties are only visible to targets defined within the same directory scope [1][2]. The DIRECTORY option overrides this behavior by applying the specified properties to the source files within each of the provided directory scopes [1][3]. Key requirements and details for using the DIRECTORY option include: 1. Prerequisite Knowledge: CMake must already be aware of the specified directories [1][3]. This means they must have been added previously via the add_subdirectory command or be the top-level source directory [1][2]. 2. Path Handling: Relative paths provided to the DIRECTORY option are interpreted as relative to the current source directory [1][3]. 3. Alternative Scope: The command also supports a TARGET_DIRECTORY option, which allows setting properties in the directory scopes where specific targets were created [1][2]. You can retrieve these properties using the get_source_file_property command [1][3]. Note that the GENERATED property is an exception to these scoping rules and may be globally visible [1][4].

Citations:


Require CMake 3.18 for the CMakeLists.txt minimum.

CMakeLists.txt declares cmake_minimum_required(VERSION 3.15), but cmake/MuiRuntimeRotation.cmake uses set_source_files_properties(... DIRECTORY ...) from lines 84-94. That call is invalid before CMake 3.18, so older compatible CMake releases fail at configure time and the rotation rename or style-routing flags are not applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/MuiRuntimeRotation.cmake` around lines 84 - 94, Update the project’s
cmake_minimum_required declaration in CMakeLists.txt from version 3.15 to 3.18
so the DIRECTORY option used by set_source_files_properties in
MuiRuntimeRotation.cmake is supported. Preserve the existing minimum-version
configuration otherwise.

Comment thread locale/cs.yml
Comment on lines +147 to +154
Orientation: ~
Landscape: ~
Portrait: ~
Landscape flipped: ~
Portrait flipped: ~
Restarting to apply: ~
Save failed: ~
Restart required: ~

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the new orientation messages in all non-English locales.

The same seven keys use ~ in each locale. Add localized values so orientation labels and restart-status messages are not shown untranslated.

  • locale/cs.yml#L147-L154: Add Czech translations.
  • locale/da.yml#L168-L175: Add Danish translations.
  • locale/de.yml#L157-L164: Add German translations.
  • locale/el.yml#L170-L177: Add Greek translations.
  • locale/es.yml#L165-L172: Add Spanish translations.
  • locale/fi.yml#L153-L160: Add Finnish translations.
  • locale/fr.yml#L160-L167: Add French translations.
  • locale/hu.yml#L170-L177: Add Hungarian translations.
📍 Affects 8 files
  • locale/cs.yml#L147-L154 (this comment)
  • locale/da.yml#L168-L175
  • locale/de.yml#L157-L164
  • locale/el.yml#L170-L177
  • locale/es.yml#L165-L172
  • locale/fi.yml#L153-L160
  • locale/fr.yml#L160-L167
  • locale/hu.yml#L170-L177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@locale/cs.yml` around lines 147 - 154, Replace the seven ~ values for
Orientation, Landscape, Portrait, Landscape flipped, Portrait flipped,
Restarting to apply, Save failed, and Restart required with localized
translations in locale/cs.yml lines 147-154, locale/da.yml lines 168-175,
locale/de.yml lines 157-164, locale/el.yml lines 170-177, locale/es.yml lines
165-172, locale/fi.yml lines 153-160, locale/fr.yml lines 160-167, and
locale/hu.yml lines 170-177; preserve the existing keys and YAML structure.

Comment on lines +15 to +19
TEST_CASE("both generated layouts are linked")
{
CHECK(&objects != &MUI2_objects);
CHECK(mui_bridge_publish_secondary != nullptr);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the bridge declaration and its generated definition.
rg -n -C 3 'mui_bridge_publish_secondary|MUI2_objects' \
  tests/test_ScreenRotation.cpp \
  .

Repository: meshtastic/device-ui

Length of output: 5397


🌐 Web query:

C++ standard address-of function pointer compare with nullptr can be true even if function deleted

💡 Result:

In C++, it is not possible to obtain a pointer to a deleted function. The C++ standard mandates that any attempt to form a pointer to a deleted function is ill-formed [1]. Because this operation is a compile-time error, you cannot evaluate such a pointer, let alone compare it with nullptr [1]. Confusion regarding this topic often stems from a misunderstanding of what a "deleted function" is in C++ versus a "deleted pointer" or an object that has been deallocated: 1. Deleted Functions (=delete): When a function is declared as deleted, the compiler removes its definition [2]. Any code that attempts to refer to this function—including calling it or taking its address—will result in a compilation error [1][3]. It is not a runtime state, so it cannot "compare" to anything at runtime [1]. 2. Deallocated Memory (delete operator): If you are referring to a pointer that held the address of an object that has since been deallocated (e.g., using the delete keyword on a heap-allocated object), that pointer becomes an invalid pointer [4][5]. Using an invalid pointer in any way, including comparing it to nullptr, results in undefined behavior [5]. While an invalid pointer might happen to compare equal to nullptr on some specific implementations or under specific circumstances, this is not standard-compliant behavior and should never be relied upon [6][5]. In summary, a deleted function cannot have an address and cannot be compared to nullptr because the code will not compile [1]. A pointer to a deallocated object is invalid, and its comparison to nullptr is undefined behavior [5].

Citations:


🌐 Web query:

libtool archive member symbol extraction test CHECK(function != nullptr) compiler optimization linker

💡 Result:

The issue you are encountering likely stems from a conflict between libtool’s legacy mechanisms for symbol management and modern compiler optimizations that assume Undefined Behavior (UB) never occurs [1][2]. Libtool historically supports "fake static preloaded modules" by extracting symbols from objects using tools like nm, then synthesizing a source file that declares these symbols to build a symbol list (lt_dlsymlist) [1]. This generated code often uses pointer-to-void casting and potentially mismatched prototypes, which modern compilers—particularly under Link Time Optimization (LTO) or aggressive optimization levels—may interpret as UB [1]. Specifically, regarding your mention of CHECK(function!= nullptr): 1. Compiler Optimization of Null Checks: Modern compilers (GCC, Clang) often assume that dereferencing a pointer or calling a member function implies the pointer is non-null [2][3]. If the compiler determines through inlining or value range propagation that a pointer must be non-null to avoid UB, it may eliminate "redundant" null checks (e.g., if(function!= nullptr)) [2][3]. If your code relies on this check to handle a scenario that the compiler deems impossible, the optimization effectively removes the safety logic, leading to crashes [4][2]. 2. LTO and Symbol Visibility: Libtool's symbol extraction sometimes struggles when LTO is involved, as LTO may rightfully complain about inconsistent declarations or unexpected visibility changes [1]. Furthermore, if libtool is not correctly handling --whole-archive or symbol visibility, the linker might discard object code containing those functions, rendering the pointers null or invalid when the runtime attempts to access them [5]. 3. Debugging/Resolution: - Verify if LTO flags (-flto) are being passed to the compilation of the generated libtool symbol files; these should generally be avoided for such synthesized code [1]. - Consider if the issue is related to the compiler assuming that symbols cannot be null. In high-performance contexts, developers sometimes use -fno-delete-null-pointer-checks to prevent the compiler from optimizing out these specific safety checks [6][3]. - Ensure that the archive member containing the function is not being garbage-collected by the linker. If the symbol is not explicitly referenced by the final executable, the linker may drop the object file entirely unless --whole-archive is used [5]. If you are seeing a crash where the null check is "optimized out," the compiler is likely treating your code as UB. You can test this by disabling optimizations for that specific unit or using the aforementioned compiler flag to confirm if the behavior is caused by aggressive null-check elimination [4][3].

Citations:


Force a retained reference to mui_bridge_publish_secondary.

A direct function pointer address cannot be null, so this test does not prove the secondary bridge object is linked. Store the address in a volatile function-pointer variable, or use an observable link anchor that cannot be optimized away.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_ScreenRotation.cpp` around lines 15 - 19, Update the “both
generated layouts are linked” test to retain and observe
mui_bridge_publish_secondary through a volatile function-pointer variable or an
equivalent non-optimizable link anchor, replacing the ineffective direct
non-null address check while preserving the existing objects linkage assertion.

Comment thread tools/gen_dual_ui.py
Comment on lines +349 to +359
def main():
check_only = "--check" in sys.argv[1:]
args = [a for a in sys.argv[1:] if not a.startswith("--")]
# accepted for symmetry with the build invocation; the output is the same
# for either primary, so it only has to be a supported view
view = args[0] if args else DEFAULT_PRIMARY
if view not in VIEWS:
sys.exit(
"ERROR: MUI_RUNTIME_ROTATION supports only %s; view %r is not one of them"
% (" and ".join(VIEWS), view)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unknown flags in main.

check_only matches only --check, and line 351 discards every other -- argument. A typo such as --chek therefore runs a full regeneration instead of a verification, and a CI gate would overwrite the committed glue rather than fail. Reject any flag that is not --check.

🐛 Proposed fix
-    check_only = "--check" in sys.argv[1:]
-    args = [a for a in sys.argv[1:] if not a.startswith("--")]
+    flags = [a for a in sys.argv[1:] if a.startswith("--")]
+    unknown = [a for a in flags if a != "--check"]
+    if unknown:
+        sys.exit("ERROR: unknown option(s): %s" % ", ".join(unknown))
+    check_only = "--check" in flags
+    args = [a for a in sys.argv[1:] if not a.startswith("--")]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def main():
check_only = "--check" in sys.argv[1:]
args = [a for a in sys.argv[1:] if not a.startswith("--")]
# accepted for symmetry with the build invocation; the output is the same
# for either primary, so it only has to be a supported view
view = args[0] if args else DEFAULT_PRIMARY
if view not in VIEWS:
sys.exit(
"ERROR: MUI_RUNTIME_ROTATION supports only %s; view %r is not one of them"
% (" and ".join(VIEWS), view)
)
def main():
flags = [a for a in sys.argv[1:] if a.startswith("--")]
unknown = [a for a in flags if a != "--check"]
if unknown:
sys.exit("ERROR: unknown option(s): %s" % ", ".join(unknown))
check_only = "--check" in flags
args = [a for a in sys.argv[1:] if not a.startswith("--")]
# accepted for symmetry with the build invocation; the output is the same
# for either primary, so it only has to be a supported view
view = args[0] if args else DEFAULT_PRIMARY
if view not in VIEWS:
sys.exit(
"ERROR: MUI_RUNTIME_ROTATION supports only %s; view %r is not one of them"
% (" and ".join(VIEWS), view)
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gen_dual_ui.py` around lines 349 - 359, Update main to validate all
arguments beginning with "--" before selecting the view, accepting only
"--check" and rejecting unknown flags with an error exit. Preserve the existing
positional view handling and check_only behavior for valid arguments.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Congratulations for your first pull request

@mverch67

mverch67 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Looking at the code (see gen_dual_ui.py, bridge_primary.c / bridge_secondary.c, ...) it looks like it is purely vibe coded without any steering into direction of minimal code changes and reusable design. This code as it is will never be maintainable and may break easily when adding new features. Also this doesn't look like a generic solution and will probably work only for a single device so I don't think it is worth to further take into consideration.
So judge by yourself: a rotated version of the firmware could be easily achieved with a second target env definition with other compiler macros set and without a single line of code changed in device-ui.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants