* Add uConsole GTK shell with SQLite map cache * Improve uConsole GTK overview dashboard * Detect uConsole hardware endpoints * Add uConsole hardware binding and map fallback * Improve uConsole settings and map UI * feat: adapt uConsole Linux shell * docs: document GPS settings and T-Deck UART noise * style: apply clang-format * site: update 0.1.25 release highlights --------- Co-authored-by: vicliu624 <vicliu@outlook.com>
16 KiB
Architecture and Repository Direction
Why this document exists
Trail Mate is no longer a single-platform firmware project. It already has:
- an Arduino + PlatformIO environment for existing ESP32 handheld devices
- an ESP-IDF-based Tab5 adaptation experiment
- a planned Linux line for Raspberry Pi class devices and UNO Q class devices
The project therefore needs a clear long-term repository structure. This document defines:
- what problem the repository structure is solving
- what the repository should look like in the future
- what kinds of code belong to shared modules vs platform adapters
- how the project should evolve to support
PIO + IDF + Linuxwithout maintaining multiple full copies of the same source tree
This is a direction document first, and an implementation plan second.
Current problem
At the moment, the project has already started moving toward reusable domain logic, but the repository still has a major structural risk:
- shared code and platform code are still mixed in the same
src/tree - some experiments have already grown into a second full project tree (
trail-mate-idf) - UI, platform entrypoints, board support, BLE, storage, timers, and system APIs are still tightly mixed in several places
- the same business logic is at risk of being copied and maintained in parallel across multiple repositories
The main waste is not lack of abstraction. The main waste is duplicate maintenance of nearly identical source trees.
If this continues, every new platform will multiply maintenance cost:
- PlatformIO firmware
- ESP-IDF firmware
- Linux simulator
- Raspberry Pi device target
- UNO Q device target
That is not sustainable.
Architecture goals
The repository structure should achieve the following goals.
1. One main repository
There should be a single main repository for Trail Mate. Platform-specific targets should become application shells inside the main repository, not separate long-term full-code forks.
2. One shared core, many shells
The project should converge toward:
- one shared business/core layer
- multiple platform adapters
- multiple application entry shells
3. No duplicate source trees
The same chat logic, GPS logic, team logic, and hostlink logic should not exist in separate copies for:
- PlatformIO
- ESP-IDF
- Linux
4. Clear platform boundaries
Core modules must not directly depend on:
- Arduino
- ESP-IDF
- FreeRTOS
- LVGL
- board-specific GPIO definitions
- hardware drivers
Those belong in platform adapters.
Shared include prefixes should reflect that boundary:
chat/...,gps/...,team/...are reserved for cross-platform module APIs- platform-specific concrete implementations should be exposed under platform-prefixed paths such as
platform/esp/arduino_common/...
5. Linux becomes a first-class target
The future Linux line is not a side experiment. The structure must allow:
linux_simfirst- then
linux_rpi - then
linux_unoq - and, when introduced, a uConsole/AIO2 desktop-class Linux handheld target governed by uconsole-aio2-linux.md
without forcing another repository split.
6. Keep migration incremental
This structure must be reachable through incremental refactoring. It must not require a one-shot rewrite of the entire project.
Target repository structure
The long-term target structure should look like this:
trail-mate/
apps/
esp_pio/
esp_idf/
targets/
tab5/
t_display_p4/
linux_sim/
linux_rpi/
linux_unoq/
linux_uconsole/
modules/
core_sys/
core_chat/
core_gps/
core_team/
core_hostlink/
ui_shared/
platform/
esp/
arduino_common/
idf_common/
boards/
tdeck/
twatchs3/
tlora_pager/
tab5/
t_display_p4/
linux/
common/
rpi/
unoq/
uconsole/
docs/
tools/
third_party/
This structure has three layers with different responsibilities.
Layer responsibilities
modules/: shared business and reusable logic
modules/* should contain code that is reusable across all environments.
Examples:
- chat domain models
- chat use cases
- contact logic
- mesh protocol state machines that do not depend on actual BLE/GATT APIs
- GPS filtering and route logic
- team/domain/protocol logic
- hostlink protocol/state logic
- persistence policies and serialization rules
- pure utility code in
sys
This layer should be the most stable and the most reusable.
Important rule
modules/* must not directly include or depend on platform headers such as:
Arduino.hPreferences.hHardwareSerialesp_*freertos/*lvgl.h- board headers
If code needs any of those, it probably does not belong in modules/*.
platform/: hardware, OS, runtime, and framework adapters
platform/* contains implementations of platform-dependent interfaces.
Examples:
- BLE transport implementations
- LoRa radio transport implementations
- GPS hardware drivers
- file system adapters
- persistent storage adapters
- timers and clock providers
- board support and GPIO mapping
- display drivers
- input drivers
- USB support
- audio codec platform glue
This layer is where platform differences belong.
Examples of likely mappings from the current tree:
src/board/*->platform/esp/boards/*src/ble/*->platform/esp/...or laterplatform/linux/...src/display/*->platform/...src/input/*->platform/...src/audio/codec/*->platform/esp/arduino_audio_codec/audio/codec/*
apps/: composition and entrypoints
apps/* should be thin application shells.
They should be responsible for:
- platform-specific startup
- selecting which boards/features are enabled
- wiring together modules and platform adapters
- providing the actual
main.cpp/ startup code - selecting build configuration
They should not become another place where business logic grows.
The goal is that each app shell is mainly composition, not implementation.
For ESP-IDF specifically, board choice should be explicit at the app-shell level via TRAIL_MATE_IDF_TARGET, not inferred from chip family macros such as CONFIG_IDF_TARGET_*, because multiple boards can share the same SoC.
Examples:
apps/esp_pio/builds existing Arduino/PlatformIO targetsapps/esp_idf/becomes the common IDF-based shell rootapps/esp_idf/targets/tab5/andapps/esp_idf/targets/t_display_p4/become the first two real IDF targetsapps/esp_idf/is the shared IDF shell root, andapps/esp_idf/targets/*carries per-target metadata and selectionapps/linux_sim/becomes the first Linux validation target
Recommended module split
The first batch of shared modules should focus on the highest reuse value.
First batch
core_syscore_chatcore_gpscore_teamcore_hostlink
Second batch
- protocol codecs
- persistence policies
- map tile and route-related pure logic
- reusable non-visual UI state/presenter logic
UI guidance
Do not attempt to fully abstract all UI at the beginning. That is high risk and low immediate return.
Instead:
- move page state and presenter-like logic gradually into
ui_shared/ - keep layout trees, LVGL object creation, and device-specific interaction details close to the app/platform layer for now
- promote only truly shared UI components, such as reusable navigation helpers, shared screen contracts, generic app-runtime helpers, and pure UI formatters
Dependency rules
The most important architectural rule is dependency direction.
Core modules may depend on:
- standard C/C++
- internal domain/usecase/port abstractions
- data model interfaces
- pure utility code
Core modules may not depend on:
- Arduino APIs
- ESP-IDF APIs
- FreeRTOS APIs
- LVGL
- board classes
- device drivers
- GPIO constants
Platform adapters may depend on:
- framework APIs
- RTOS APIs
- hardware drivers
- shared module interfaces
Apps may depend on:
- shared modules
- platform adapters
- build/runtime configuration
This keeps the structure stable as the number of supported environments grows.
Port and adapter model
The project already contains good early examples of the intended direction.
Examples already visible in the codebase include patterns such as:
- usecase
- ports/interfaces
- infra/adapter implementations
This should become the default pattern for shared logic:
module/
domain/
ports/
usecase/
infra/ # only if still platform-neutral
If an infra implementation depends on Arduino, ESP-IDF, FreeRTOS, LVGL, or board-specific APIs, it should move out of the module and into platform/*.
Build system direction
CMake should define shared module structure
CMake is the most natural common build description because:
- ESP-IDF already uses CMake
- Linux targets naturally use CMake
- explicit targets make module boundaries clearer than large recursive source globs
Each shared module should eventually become an explicit library target.
Why this matters
The current pattern of globally collecting large parts of src/ is convenient in the short term but makes long-term reuse harder.
If Linux becomes a real target, module boundaries need to be visible in the build system, not just in folder names.
PlatformIO should become a build shell, not a separate source universe
PlatformIO should continue to exist, but only as:
- a convenient build and flashing entrypoint for ESP targets
- a consumer of shared
modules/*plusplatform/esp/*
It should not justify keeping a second copy of core logic.
Environment strategy
apps/esp_pio
Purpose:
- continue supporting existing handheld ESP targets under PlatformIO
- reuse shared modules and platform adapters
apps/esp_idf
Purpose:
- host the shared ESP-IDF app shell for multiple ESP-IDF boards
- validate the common architecture against a non-Arduino ESP runtime
- keep per-board target descriptors under
apps/esp_idf/targets/*instead of growing one full shell per board
This should replace the need for separate long-term IDF-side full projects.
apps/linux_sim
Purpose:
- validate the shared core outside ESP
- enable faster iteration without board flashing
- become the first proof that the architecture is genuinely portable
This target should come before linux_rpi and linux_unoq.
apps/linux_rpi and apps/linux_unoq
Purpose:
- provide actual Linux device targets after the simulator proves the shared core is portable
The platform differences here should stay in platform/linux/*, not leak back into shared modules.
apps/linux_uconsole
Purpose:
- provide the desktop-class Linux handheld shell for uConsole/AIO2-class targets
- preserve a separate interaction model from compact Cardputer Zero shells
- consume Linux app services and presentation models instead of compact LVGL pages
- leave room for Linux-native search, diagnostics, data/package management, and background-job workflows
- use SQLite-backed local state and Linux-native map tile cache/indexing where those capabilities are outside MCU constraints
AIO2 support belongs below this app shell in platform/runtime adapters. The UI may report AIO2 capability status, but AIO2 must not become the product navigation or layout boundary.
The current Linux service composition entrypoint is LinuxAppServices under
platform/linux/common. Compact LVGL shells use MinimalLinuxAppFacade as an
adapter over those services; uConsole shells should depend on
LinuxAppServices and presentation models directly.
Migration principles
This repository should not be restructured through a one-shot rewrite. The migration should be incremental.
Principle 1: stop growing duplicate trees
Once the repository direction is accepted, avoid continuing to evolve multiple full source trees independently.
Principle 2: move boundaries before moving everything
It is more important to establish the correct dependency boundaries than to instantly move every file into its final directory.
Principle 3: migrate by vertical slice
Migrate one logical area at a time, such as:
core_sys- then
core_chat - then
core_gps - then
core_team - then
core_hostlink
Each slice should still build somewhere before the next one starts.
Principle 4: prefer thin wrappers during transition
Temporary wrappers, forwarding headers, and compatibility glue are acceptable if they reduce migration risk.
Principle 5: Linux simulation is a milestone
A module is not truly portable until it has been exercised in a non-ESP environment.
Recommended migration order
A practical order for this repository is:
Phase 1: establish skeleton and rules
- create
apps/,modules/, andplatform/directory skeletons - define module boundaries in documentation
- stop adding new cross-platform logic directly into mixed platform code where possible
Phase 2: extract core_sys
- isolate generic time/utility/config-independent helpers
- introduce common interfaces like
IClock,ITimer,IStorage,IFileSystemwhere needed
Phase 3: extract core_chat
- move chat domain/usecase/port logic into a shared module
- keep storage/network/BLE/LoRa side effects in platform adapters
Phase 4: extract core_gps
- move GPS usecase/filtering/route logic into shared core
- keep hardware access and serial/device specifics in platform adapters
Phase 5: extract core_team and core_hostlink
- move protocol logic and state machines into modules
- keep radio and transport specifics in platform adapters
Phase 6: create linux_sim
- build shared modules outside ESP
- use mock or simulated platform adapters
- prove the architecture works before targeting real Linux hardware
Phase 7: converge on apps/esp_idf + IDF targets
- reduce the separate IDF experiments into one shared app shell plus explicit per-board targets
Phase 8: add real Linux device targets
linux_rpilinux_unoq
What success looks like
The repository structure is successful when all of the following are true:
- adding a new platform does not require duplicating the whole source tree
- chat/GPS/team/hostlink logic is implemented once
- platform-specific details stay in adapters and app shells
- PlatformIO and ESP-IDF both build from the same shared core modules
- Linux simulation can exercise core logic without ESP headers or Arduino runtime
- new contributors can tell where code should go without guessing
Practical contributor rules
When adding new code, use these rules.
Put code in modules/* if it is:
- business logic
- protocol logic without direct platform APIs
- reusable data transformation or state management
- portable across ESP and Linux
Put code in platform/* if it touches:
- Arduino
- ESP-IDF
- FreeRTOS
- LVGL
- GPIO
- BLE stack APIs
- serial ports
- SD/flash implementation details
- audio codec drivers
- display/input drivers
Put code in apps/* if it is about:
- startup
- board selection
- feature composition
- dependency wiring
- build target selection
Avoid in shared code:
#ifdef ARDUINO...#ifdef ESP_PLATFORMmillis()PreferencesHardwareSerialesp_*lvgl
If you need those directly, the code probably belongs in a platform adapter.
Final note
This document describes the architectural direction of Trail Mate. It does not require every file to move immediately.
The key idea is simple:
one main repository, one shared core, many platform shells
That is the only sustainable way for Trail Mate to support:
- existing PlatformIO handheld firmware
- ESP-IDF-based targets
- Linux simulator targets
- future Raspberry Pi and UNO Q devices
without turning repository maintenance into duplicated work.