Files
trail-mate/docs/REPOSITORY_MIGRATION_PLAN.md
T
vicliu bf7068b02b 0.1.26-alpha release
* refactor: render chat rows from presentation state

* Fix Meshtastic channel sync and add MeshCore CN preset

* Add granular chat notification settings

* Add SD settings backup and restore

* Prepare 0.1.26-alpha release

---------

Co-authored-by: vicliu624 <vicliu@outlook.com>
2026-05-19 16:04:42 +08:00

84 KiB

#Repository Migration Plan

This document translates the architectural direction into a practical migration path for the current repository.

Current execution plan

For the current UI/shared-page migration and ESP-IDF compatibility work, use docs/UI_SHARED_IDF_PHASE_PLAN.md as the active execution plan.

That document supersedes any "minimal shell first" interpretation for page migration. The current rule is:

  • one page, one shared implementation
  • app layer only owns startup / loop / runtime / adapter composition
  • PlatformIO and ESP-IDF must advance together for each migration phase

Current status snapshot:

  • phases 0 / 1 / 2 / 3 / 5 / 6 in docs/UI_SHARED_IDF_PHASE_PLAN.md are now complete
  • phase 4 remains the active workstream for finishing the remaining business-page migrations

Cross-platform product architecture baseline

Phase 1 of the product architecture work is now defined as:

Phase 1: Establish the cross-platform product architecture baseline.

This phase does not extract LoRa, GPS, BLE, or UI behavior. It establishes the shared language and review surface that later extractions must use:

  • docs/specification/CROSS_PLATFORM_PRODUCT_ARCHITECTURE_SPEC.md
  • docs/specification/CAPABILITY_AUTHORITY_GLOSSARY.md
  • docs/specification/TARGET_MANIFEST_SPEC.md
  • docs/specification/BOARD_PACKAGE_SPEC.md
  • docs/specification/RUNTIME_CONCURRENCY_SPEC.md
  • docs/specification/UI_PRESENTATION_ARCHITECTURE_SPEC.md
  • draft target manifests under docs/targets/
  • draft board manifests under docs/boards/
  • vocabulary-only device types under modules/core_device/
  • review checklist under docs/review/
  • non-blocking boundary report under tools/architecture/

The operating rule for all later migration steps is:

Target chooses.
Board describes.
Platform adapts.
Runtime schedules.
Capability provides.
Protocol interprets.
UseCase decides.
AppService coordinates.
PresentationModel projects.
Renderer draws.
CompositionRoot wires.

The new boundary checker is intentionally report-only during Phase 1. Known historical drift is recorded in tools/architecture/KNOWN_PRODUCT_ARCHITECTURE_VIOLATIONS.md and must be paid down in later phases rather than hidden inside this baseline step.

Current authoritative layout

Today, the repository is still primarily organized around a single src/ tree plus root-level build entrypoints:

  • src/ contains a mix of shared logic, platform logic, UI, board support, and runtime glue
  • platformio.ini is the main active PlatformIO build entrypoint
  • root CMakeLists.txt currently acts as a simple IDF-oriented entrypoint
  • variants/, boards/, and board-specific code still reflect the current ESP-oriented structure

This remains the active structure during the migration.

Target direction

The repository is converging toward:

  • apps/ for application shells
  • modules/ for shared business/core logic
  • platform/ for runtime/framework/hardware adapters

Migration philosophy

The migration should be incremental. We should avoid a large one-shot move of the entire src/ tree.

The repository should remain buildable during each major step.

Initial mapping guide

The following mapping is directional rather than immediate. It describes where code is expected to end up over time.

Current area Target area Notes
src/chat/domain, src/chat/usecase, src/chat/ports modules/core_chat Shared logic first
src/gps/usecase, src/gps/ports modules/core_gps Hardware-independent logic first
src/team/domain, src/team/protocol, selected usecase logic modules/core_team Keep transport side effects out
src/hostlink protocol/state logic modules/core_hostlink Keep transport adapters out
generic src/sys utilities modules/core_sys Filter out framework dependencies
src/board/* platform/esp/boards/* Board support belongs in platform layer
src/ble/* platform/esp/... and later platform/linux/... Split protocol logic from stack integration
src/display/*, src/input/*, src/usb/* platform/* Runtime/framework dependent
src/audio/codec/* platform/esp/arduino_audio_codec/audio/codec/* ESP audio codec stack and platform glue
root startup / main.cpp composition apps/* App shell responsibility

First practical extraction order

Step 1

Create the new directory skeleton and establish the rules.

Step 2

Extract core_sys as the lowest-risk shared foundation.

Step 3

Extract core_chat and keep storage/network/radio adapters in the platform layer.

Step 4

Extract core_gps while keeping hardware access in adapters.

Step 5

Extract core_team and core_hostlink.

Step 6

Introduce apps/linux_sim to validate that extracted shared modules are genuinely portable.

What not to do during early migration

Do not:

  • maintain a second full source tree for IDF long term
  • move all UI into a generic shared layer immediately
  • copy shared code into Linux-only directories
  • let new platform-dependent business logic continue to grow inside modules/*

First concrete migration landed

The first real code migration after the directory skeleton is TwoPane UI infrastructure:

  • src/ui/components/two_pane_* moved into modules/ui_shared
  • PlatformIO now sees modules/* as a library root
  • IDF now includes modules/ui_shared/include and compiles modules/ui_shared/src

This is intentionally small in scope, but it proves the new repository layout can host shared code without duplicating it across separate project trees.

Second concrete migration landed

The first core_sys extraction is now in place:

  • src/sys/ringbuf.h moved into modules/core_sys/include/sys/ringbuf.h
  • existing chat code now consumes sys/ringbuf.h from core_sys
  • event_bus stays in the legacy tree until its chat/team coupling is split apart

This keeps the migration incremental while starting to establish a real low-level shared foundation.

Third concrete migration landed

The first core_chat extraction is now in place:

  • chat domain and ports headers moved into modules/core_chat/include
  • selected legacy use-case headers now include module paths directly
  • legacy headers under src/chat/domain and src/chat/ports remain as compatibility shims
  • migrated chat domain headers no longer depend on Arduino.h for basic integer types

This establishes a real shared contract layer for chat without forcing a one-shot move of chat implementations.

Fourth concrete migration landed

The next core_chat slice is now in place:

  • chat/usecase public headers moved into modules/core_chat/include
  • chat/time_utils.h moved into modules/core_chat/include/chat/time_utils.h
  • chat_model.cpp and contact_service.cpp moved into modules/core_chat/src
  • chat_service.cpp remains in the legacy tree for now because it still publishes through legacy event_bus

This means the chat module now contains both shared contracts and part of the real executable use-case/domain implementation.

Fifth concrete migration landed

chat_service.cpp is now decoupled from the legacy event bus:

  • chat_service.cpp moved into modules/core_chat/src
  • ChatService now exposes a high-level incoming-message observer hook
  • a legacy bridge under src/chat/infra translates that hook back into sys::EventBus events for the current app shell

This is an important boundary shift: shared chat logic no longer needs to know about the legacy global event bus.

Sixth concrete migration landed

The generic in-memory chat store is now shared:

  • RamStore moved into modules/core_chat
  • legacy include path src/chat/infra/store/ram_store.h remains as a compatibility shim
  • platform-backed stores (ContactStore, FlashStore, LogStore) remain in the legacy tree for now

This keeps the shared module focused on portable chat logic while leaving storage backends with platform dependencies in place until they can be split more cleanly.

Seventh concrete migration landed

The first core_gps slice is now in place:

  • portable GPS domain headers moved into modules/core_gps/include
  • i_gps_hw.h moved into modules/core_gps/include/gps/ports
  • gps_jitter_filter moved into modules/core_gps as the first shared GPS implementation
  • legacy headers remain as compatibility shims

This starts a real shared GPS core without dragging the current board/runtime-specific service layer into the module prematurely.

Eighth concrete migration landed

The next core_gps slice now includes motion policy logic:

  • i_motion_hw.h moved into modules/core_gps/include/gps/ports
  • MotionPolicy moved into modules/core_gps
  • the motion hardware contract no longer exposes the Bosch callback typedef directly
  • the ESP motion adapter now translates to the generic motion callback type

This is another meaningful boundary improvement because Bosch-specific types are no longer part of the shared GPS contract surface.

Ninth concrete migration landed

The next core_gps slice extracts runtime policy decisions:

  • gps/usecase/gps_runtime_policy.* moved into modules/core_gps
  • GPS collection interval normalization is now shared logic
  • motion idle-time normalization is now shared logic
  • GPS power on/off decision logic is now shared logic
  • legacy GpsService remains as the FreeRTOS and adapter shell

This keeps the runtime shell in place while moving more GPS behavior into a platform-neutral module boundary.

Tenth concrete migration landed

The next core_gps slice extracts GNSS/NMEA runtime configuration state:

  • gps/usecase/gps_runtime_config.* moved into modules/core_gps
  • GNSS config values and pending state are now shared logic
  • NMEA config values and pending state are now shared logic
  • legacy GpsService still owns the actual hardware apply calls

This continues the pattern of shrinking GpsService into a runtime shell while moving portable state handling into core_gps.

Eleventh concrete migration landed

The next core_gps slice extracts portable runtime state:

  • gps/usecase/gps_runtime_state.* moved into modules/core_gps
  • requested GPS collection interval is now shared state
  • power strategy and team-mode flags are now shared state
  • motion-control enabled / armed state is now shared state
  • legacy GpsService still owns powered/time-synced flags and task orchestration

This narrows GpsService further toward a pure platform runtime shell while preserving behavior.

Twelfth concrete migration landed

The first core_team slice is now in place:

  • team domain headers moved into modules/core_team/include
  • team ports headers moved into modules/core_team/include
  • portable team protocol headers and codecs moved into modules/core_team
  • legacy headers under src/team/domain, src/team/ports, and src/team/protocol remain as compatibility shims

This establishes a real shared team contract/protocol layer without pulling the current runtime/event-bus shell into the module too early.

Chat shell split milestone landed

The remaining legacy src/chat tree has now been fully retired from the active build:

  • platform-bound chat/runtime shells moved to platform/esp/arduino_common/include/platform/esp/arduino_common/chat/... and platform/esp/arduino_common/src/chat/...
  • platform-neutral chat core remains in modules/core_chat
  • codebase includes canonical chat/... paths instead of ../chat/... legacy relative paths
  • platform-specific chat concrete headers no longer reuse the shared chat/... public prefix
  • src/chat compatibility shims and stub sources have been removed

This is the first point where the repository matches the intended direction more closely: chat is now split between modules/ shared core and platform/ runtime shells, instead of being duplicated under a legacy src/chat tree.

Thirteenth concrete migration landed

The next core_team slice moves the main team use case into the shared module:

  • team/usecase/team_service.* moved into modules/core_team
  • a new team/ports/i_team_runtime.h isolates time and random dependencies
  • legacy event-bus publishing for unknown app-data now lives in a bridge under src/team/infra/event
  • legacy Arduino time/random access now lives in a runtime adapter under src/team/infra/runtime

This makes the main team service a real shared use case while keeping platform behavior in thin legacy adapters.

Fourteenth concrete migration landed

The next core_team slice moves more use-case logic into the shared module:

  • team/usecase/team_controller.* moved into modules/core_team
  • team/usecase/team_track_sampler.* moved into modules/core_team
  • a new team/ports/i_team_track_source.h isolates GPS sampling for team track generation
  • legacy GPS/time access now lives in runtime and track-source adapters under src/team/infra/runtime

This keeps the shared team use-case layer growing while leaving ESP-NOW pairing as a platform-side shell for now.

Fifteenth concrete migration landed

The first core_hostlink slice is now in place:

  • hostlink_types.h moved into modules/core_hostlink/include/hostlink
  • hostlink_codec.h moved into modules/core_hostlink/include/hostlink
  • hostlink_codec.cpp moved into modules/core_hostlink/src/hostlink
  • legacy include paths under src/hostlink remain as compatibility shims

This establishes a real shared hostlink protocol/codec boundary while keeping USB transport, FreeRTOS runtime, configuration mutation, GPS polling, and event-bus integration in the legacy shell.

Sixteenth concrete migration landed

The next core_hostlink slice moves more pure hostlink logic into the shared module:

  • hostlink_config_codec.h moved into modules/core_hostlink/include/hostlink
  • hostlink_config_codec.cpp moved into modules/core_hostlink/src/hostlink
  • status TLV payload encoding is now shared logic
  • config TLV decoding is now shared logic
  • legacy hostlink_config_service.cpp now acts as the thin shell that gathers runtime state, applies decoded config patches to AppContext, and performs settimeofday

This narrows the hostlink shell further toward transport/runtime integration while keeping portable protocol mapping in core_hostlink.

Seventeenth concrete migration landed

The next core_hostlink slice moves more service-side pure logic into the shared module:

  • hostlink_service_codec.h moved into modules/core_hostlink/include/hostlink
  • hostlink_service_codec.cpp moved into modules/core_hostlink/src/hostlink
  • HELLO_ACK payload construction is now shared logic
  • GPS event payload construction is now shared logic
  • CmdTxMsg and CmdTxAppData payload parsing is now shared logic

The legacy hostlink_service.cpp still owns USB CDC I/O, FreeRTOS queues/tasks, command execution against app/team services, and periodic scheduling.

Eighteenth concrete migration landed

The next core_hostlink slice extracts hostlink session state bookkeeping:

  • hostlink_session.h moved into modules/core_hostlink/include/hostlink
  • hostlink_session.cpp moved into modules/core_hostlink/src/hostlink
  • link state, counters, TX sequence, handshake deadline, and periodic timer state are now shared structs/helpers
  • the legacy hostlink_service.cpp now uses the shared session helpers for handshake progress, periodic status/GPS timing, error bookkeeping, and TX/RX counters

The runtime shell still owns USB transport, queue lifecycle, decoder lifecycle, and command execution.

Nineteenth concrete migration landed

The next core_hostlink slice extracts frame routing and readiness gating:

  • hostlink_frame_router.h moved into modules/core_hostlink/include/hostlink
  • hostlink_frame_router.cpp moved into modules/core_hostlink/src/hostlink
  • HELLO handling vs. not-ready rejection is now decided in shared logic
  • frame-type to command-kind dispatch is now decided in shared logic

The legacy hostlink_service.cpp still executes commands and performs I/O, but it no longer owns the pure decision logic for how an incoming frame should be routed.

Immediate repository status after phase 1

After the first phase, the repository may still build exactly as before, but it will have:

  • a documented architectural direction
  • a target directory skeleton
  • a defined migration map
  • a clearer place for future extractions

That is intentional. The first phase is about reducing ambiguity before moving more code.

Twentieth concrete migration landed

The next core_hostlink slice extracts app-data frame encoding helpers:

  • hostlink_app_data_codec.h moved into modules/core_hostlink/include/hostlink
  • hostlink_app_data_codec.cpp moved into modules/core_hostlink/src/hostlink
  • RX metadata TLV encoding is now shared logic
  • EvAppData frame chunking/header assembly is now shared logic

The legacy hostlink_bridge_radio.cpp still owns event-bus observation, runtime team-state tracking, and final event enqueueing, but it no longer owns the pure binary encoding of app-data frames.

Twenty-first concrete migration landed

The next core_hostlink slice extracts more bridge-side pure payload encoding:

  • hostlink_event_codec.h moved into modules/core_hostlink/include/hostlink
  • hostlink_event_codec.cpp moved into modules/core_hostlink/src/hostlink
  • EvRxMsg payload encoding is now shared logic
  • EvTxResult payload encoding is now shared logic
  • EvTeamState payload encoding and payload hashing are now shared logic

The legacy hostlink_bridge_radio.cpp still owns event-bus subscription, runtime team-state tracking, and event selection, but the binary event payload layout is now mostly delegated into core_hostlink.

Twenty-second concrete migration landed

The next core_chat slice extracts two Meshtastic pure helpers:

  • chat/infra/meshtastic/mt_region.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_region.cpp moved into modules/core_chat/src/infra/meshtastic
  • chat/infra/meshtastic/mt_dedup.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_dedup.cpp moved into modules/core_chat/src/infra/meshtastic
  • Meshtastic region/frequency estimation is now shared logic
  • Meshtastic packet deduplication is now shared logic and no longer depends directly on Arduino.h

Legacy headers under src/chat/infra/meshtastic remain as compatibility shims, while runtime radio/protobuf adapter code continues to stay in the legacy shell until more of the protocol layer is separated cleanly.

Twenty-third concrete migration landed

The next core_chat slice extracts a MeshCore pure helper:

  • chat/infra/meshcore/mc_region_presets.h moved into modules/core_chat/include/chat/infra/meshcore
  • chat/infra/meshcore/mc_region_presets.cpp moved into modules/core_chat/src/infra/meshcore
  • MeshCore region preset lookup/validation is now shared logic

The active MeshCore adapter still remains in the legacy shell, but its static preset table is now part of the shared chat protocol layer.

Twenty-fourth concrete migration landed

The next core_chat slice extracts the Meshtastic wire-packet codec:

  • chat/infra/meshtastic/mt_packet_wire.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_packet_wire.cpp moved into modules/core_chat/src/infra/meshtastic
  • Meshtastic wire header parsing is now shared logic
  • Meshtastic AES-CTR packet payload encryption/decryption is now shared logic

The active Meshtastic adapter still remains in the legacy shell, but its on-air wire packet format handling is now part of the shared chat protocol layer.

Twenty-fifth concrete migration landed

The next core_chat slice extracts the Meshtastic protobuf codec layer:

  • chat/infra/meshtastic/mt_codec_pb.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_codec_pb.cpp moved into modules/core_chat/src/infra/meshtastic
  • chat/infra/meshtastic/compression/unishox2.h moved into modules/core_chat/include/chat/infra/meshtastic/compression
  • chat/infra/meshtastic/compression/unishox2.cpp moved into modules/core_chat/src/infra/meshtastic/compression
  • Meshtastic protobuf payload encode/decode is now shared logic
  • Local text decompression support is now part of the shared chat protocol layer

Legacy headers under src/chat/infra/meshtastic remain as compatibility shims, while the runtime Meshtastic adapter still remains in the legacy shell.

Twenty-sixth concrete migration landed

The next core_chat slice extracts the pure MeshCore identity crypto helpers:

  • chat/infra/meshcore/meshcore_identity_crypto.h moved into modules/core_chat/include/chat/infra/meshcore
  • chat/infra/meshcore/meshcore_identity_crypto.cpp moved into modules/core_chat/src/infra/meshcore
  • public-key derivation, signing, verification, and shared-secret derivation are now shared logic
  • the legacy meshcore_identity.cpp now keeps persistence/random/runtime concerns only

Twenty-seventh concrete migration landed

The next core_chat slice moves the embedded MeshCore ed25519 implementation under the shared module:

  • chat/infra/meshcore/crypto/ed25519/* moved into modules/core_chat/src/infra/meshcore/crypto/ed25519
  • chat/infra/meshcore/crypto/ed25519/ed_25519.h is now exposed from modules/core_chat/include/chat/infra/meshcore/crypto/ed25519
  • the MeshCore crypto helper layer no longer needs to reach back into the legacy src/ tree for its implementation

Twenty-eighth concrete migration landed

The next core_chat slice extracts shared Meshtastic protocol helpers that were still embedded in the runtime adapter:

  • chat/infra/meshtastic/mt_protocol_helpers.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_protocol_helpers.cpp moved into modules/core_chat/src/infra/meshtastic
  • ACK gating for broadcast vs. direct sends is now shared logic
  • traceroute hop/SNR helper assembly is now shared logic
  • protobuf string field draining and encrypted-packet reconstruction are now shared logic
  • modem-preset to radio-parameter mapping and channel hashing helpers are now shared logic
  • PSK expansion, packet hex formatting, and routing/key-verification log helpers are now shared logic

The active mt_adapter.cpp still owns runtime radio I/O, timers, queues, storage, and app/event integration, but more of its pure protocol behavior now delegates into core_chat.

Twenty-ninth concrete migration landed

The next core_chat slice extracts shared MeshCore protocol helpers that were still embedded in the runtime adapter:

  • chat/infra/meshcore/meshcore_protocol_helpers.h moved into modules/core_chat/include/chat/infra/meshcore
  • chat/infra/meshcore/meshcore_protocol_helpers.cpp moved into modules/core_chat/src/infra/meshcore
  • MeshCore packet header parsing/building is now shared logic
  • MeshCore frame hashing and packet signature calculation are now shared logic
  • MeshCore airtime/timeout/delay estimation helpers are now shared logic
  • MeshCore key-zero checks, key material expansion, channel hashing, hex formatting, and encrypt/MAC framing helpers are now shared logic

The active meshcore_adapter.cpp still owns runtime radio I/O, scheduling, persistence, and event integration, but more of its pure protocol behavior now delegates into core_chat.

Thirtieth concrete migration landed

The next core_chat slice extracts the shared node-store blob format helpers used by the Meshtastic node persistence shell:

  • chat/infra/node_store_blob_format.h moved into modules/core_chat/include/chat/infra
  • chat/infra/node_store_blob_format.cpp moved into modules/core_chat/src/infra
  • node-store blob size/count validation is now shared logic
  • node-store SD header build/validation is now shared logic
  • node-store blob metadata and CRC validation is now shared logic

The legacy src/chat/infra/meshtastic/node_store.cpp still owns SD/NVS I/O and fallback policy, but its blob-format rules now delegate into core_chat.

Thirty-first concrete migration landed

The next cleanup keeps the legacy storage shell thinner without pushing Arduino persistence into modules/*:

  • src/chat/infra/blob_store_io.h/.cpp now centralizes raw SD/Preferences blob load/save helpers
  • the legacy src/chat/infra/contact_store.cpp now delegates raw blob I/O to that shared shell helper
  • this removes duplicated Arduino storage boilerplate while preserving the current ESP-facing persistence boundary

Thirty-second concrete migration landed

The next cross-module cleanup removes temporary core_team -> core_chat shadow headers:

  • modules/core_team/include/team/* headers now include core_chat contracts directly from modules/core_chat/include/...
  • temporary compatibility headers under modules/core_team/include/chat/* are removed
  • core_team now relies on its declared dependency on core_chat instead of re-exporting chat headers locally

This keeps module boundaries cleaner and avoids reintroducing duplicate include trees inside modules/*.

Thirty-third concrete migration landed

The next shell cleanup extends the shared blob-storage helper so more legacy persistence code can collapse around one place:

  • src/chat/infra/blob_store_io.* now supports optional Preferences-side version/CRC metadata keys
  • the legacy src/chat/infra/meshtastic/node_store.cpp now delegates its raw NVS blob + metadata I/O to that shared shell helper
  • ContactStore and NodeStore public headers no longer pull SD.h / Preferences.h into all downstream includes

This keeps platform persistence in the legacy shell while shrinking duplicated storage boilerplate and header coupling.

Thirty-fourth concrete migration landed

The next core_chat slice extracts Meshtastic PKI AES-CCM helpers that were still embedded in the runtime adapter:

  • chat/infra/meshtastic/mt_pki_crypto.h moved into modules/core_chat/include/chat/infra/meshtastic
  • chat/infra/meshtastic/mt_pki_crypto.cpp moved into modules/core_chat/src/infra/meshtastic
  • PKI shared-secret hashing is now shared logic
  • PKI nonce construction is now shared logic
  • PKI AES-CCM encrypt/decrypt helper flow is now shared logic
  • PKI key-verification hash derivation is now shared logic

The active mt_adapter.cpp still owns Curve25519 key exchange, runtime node-key lookup, and packet routing behavior, but its pure PKI payload crypto flow now delegates into core_chat.

Thirty-fifth concrete migration landed

The next core_chat slice extracts shared MeshCore payload/discovery helpers that were still embedded in the runtime adapter:

  • chat/infra/meshcore/meshcore_payload_helpers.h moved into modules/core_chat/include/chat/infra/meshcore
  • chat/infra/meshcore/meshcore_payload_helpers.cpp moved into modules/core_chat/src/infra/meshcore
  • public-channel fallback and channel-key selection helpers are now shared logic
  • MeshCore frame/payload shape checks and datagram/frame builders are now shared logic
  • direct/group app payload decode, advert/discover decode, node-id derivation, and verification-code formatting are now shared logic

The active meshcore_adapter.cpp still owns runtime radio flow, timers, path scheduling, and persistence, but more of its payload/discovery behavior now delegates into core_chat.

Thirty-sixth concrete migration landed

The next shell cleanup reuses the shared Preferences blob helper for versioned key stores:

  • the legacy meshcore_adapter.cpp peer-public-key store now uses blob_store_io for raw Preferences blob/version handling
  • the legacy mt_adapter.cpp PKI node-key store now uses blob_store_io for raw Preferences blob/version handling
  • this keeps versioned Preferences I/O policy in one place while leaving MeshCore/Meshtastic entry validation and migration behavior in their runtime shells

Thirty-seventh concrete migration landed

The next shell cleanup reuses the shared blob helper for local keypair persistence too:

  • the legacy meshcore_identity.cpp now uses blob_store_io for private/public key Preferences persistence
  • the legacy mt_adapter.cpp now uses blob_store_io for local PKI public/private key Preferences persistence
  • this further shrinks direct Preferences boilerplate without moving device-local key ownership out of the runtime shells

Thirty-eighth concrete migration landed

The next core_chat slice moves the Meshtastic generated nanopb definitions out of the legacy tree:

  • src/chat/infra/meshtastic/generated/* moved into modules/core_chat/src/* (meshtastic/*.pb.* plus shared nanopb support files)
  • Meshtastic .pb.cpp/.h definitions and shared nanopb support C files now compile as part of core_chat
  • legacy BLE/UI/runtime code now includes the generated protocol headers through the shared include root instead of reaching into src/chat/infra/meshtastic/generated directly

This keeps Meshtastic protocol schema/codegen with the rest of the shared chat protocol layer, while the active runtime adapters still remain in the legacy shell.

Thirty-ninth concrete migration landed

The next app-shell cleanup restructures AppContext without moving platform runtime ownership out of src/app yet:

  • AppContext::init() is decomposed into focused shell helpers for GPS runtime, track recorder, chat store/model, mesh backend setup, team runtime, contact services, and BLE startup
  • mesh backend creation is now centralized through one private helper reused by both initial startup and runtime protocol switching
  • the public AppContext surface remains behavior-compatible, while the bootstrap flow is now easier to split into future apps/* entrypoints

This keeps the active Arduino/ESP app composition in the legacy shell, but makes the bootstrap layer substantially easier to untangle in later migration steps.

Fortieth concrete migration landed

The next core_chat slice extracts repeated mesh-protocol utility logic that had started to drift across app, hostlink, BLE, store, and UI shells:

  • chat/infra/mesh_protocol_utils.h moved into modules/core_chat/include/chat/infra
  • chat/infra/mesh_protocol_utils.cpp moved into modules/core_chat/src/infra
  • mesh-protocol validity checks, raw-value normalization, full labels, short labels, and slug strings are now shared logic
  • legacy app/hostlink/BLE/UI/store code now reuses that helper instead of open-coding protocol validation and display strings

This keeps protocol-selection presentation and normalization rules consistent across shells while staying fully platform-agnostic inside core_chat.

Forty-first concrete migration landed

The next app-shell cleanup extracts shared screen-timeout persistence and normalization logic out of main.cpp:

  • src/screen_sleep.cpp now owns timeout clamping, persisted Preferences I/O, and the runtime timeout cache
  • src/screen_sleep.h now exposes the shared timeout helpers used by main runtime, BLE config sync, and settings UI
  • legacy main.cpp keeps the actual sleep/screen-saver task behavior, but no longer owns duplicate timeout persistence helpers
  • meshtastic_ble.cpp now reuses the same timeout clamp/read logic instead of carrying its own copy of the settings constants and preferences reads

This reduces duplication in the ESP shell and makes a later screen/runtime split out of main.cpp much more mechanical.

Forty-second concrete migration landed

The next app-shell cleanup finishes extracting the active screen-sleep runtime out of main.cpp:

  • src/screen_sleep.h/.cpp now own the screen-saver layer, activity mutex, sleep task, and wake/saver state transitions
  • main.cpp now only provides tiny UI hooks (menu return, unread count, watch-face wake callback) and calls one initScreenSleepRuntime(...) entrypoint
  • BLE, settings UI, LVGL input paths, and the main runtime now all consume one shared screen-sleep shell API instead of spreading state across multiple translation units

This keeps the ESP-specific runtime in src/*, but removes another large stateful subsystem from the startup entrypoint and makes a future apps/* split more mechanical.

Forty-third concrete migration landed

The next app-shell cleanup extracts the application registry out of main.cpp:

  • src/ui/app_registry.h/.cpp now own the FunctionAppScreen wrapper plus the board/feature-gated application list
  • main.cpp no longer carries screen icon declarations, app enter/exit forward declarations, or the large conditional kAppScreens[] registry block
  • menu construction now reads the registry through ui::appCatalog(), leaving main.cpp focused on menu layout and runtime wiring

This keeps the active LVGL menu shell behavior unchanged, while making the startup/menu entrypoint smaller and easier to reuse from future apps/* targets.

Forty-fourth concrete migration landed

The next app-shell cleanup extracts low-battery guard behavior out of main.cpp:

  • src/app/battery_guard.h/.cpp now own battery-tier degradation, warning cooldowns, and critical auto-shutdown scheduling
  • main.cpp now just feeds battery samples into app::handleLowBattery(...) during immediate setup and periodic UI refresh
  • board-facing shutdown/tier behavior stays in the ESP shell, but no longer sits inline inside the startup/menu entrypoint

This removes another stateful runtime concern from main.cpp and makes the remaining startup code more about composition than policy.

Forty-fifth concrete migration landed

The next app-shell cleanup extracts the menu top-bar/watch-face runtime out of main.cpp:

  • src/ui/menu_runtime.h/.cpp now own the menu top bar, time/battery labels, menu status row registration, periodic refresh timers, and T-Watch watch-face wiring
  • main.cpp now only provides the shared time-format hook and wires ui::menu_runtime::init(...) plus the screen-sleep wake callback
  • screen-sleep wake behavior now re-enters through ui::menu_runtime::onWakeFromSleep(), keeping watch-face behavior with the rest of the menu runtime instead of the startup entrypoint

This removes another chunk of UI runtime state from main.cpp and keeps the startup file focused on composing shells instead of owning their timers and widget trees directly.

Forty-sixth concrete migration landed

The next app-shell cleanup extracts the board/app bootstrap flow out of main.cpp:

  • src/app/bootstrap.h/.cpp now own wakeup-cause detection helpers, board bring-up, wake-from-sleep board restore, and the board-specific AppContext::init(...) wiring
  • main.cpp now just logs startup state, calls the bootstrap helpers, and continues with LVGL/menu/runtime composition
  • board-selection preprocessor branches still exist in the ESP shell, but they are now concentrated in one bootstrap unit instead of being embedded inline in the entrypoint

This makes the remaining main.cpp much closer to a pure composition root and reduces one more large block of platform-condition-heavy code.

Forty-seventh concrete migration landed

The next app-shell cleanup extracts the remaining menu layout/composition block out of main.cpp:

  • src/ui/menu_layout.h/.cpp now own the menu tileview, menu/app panels, LVGL groups, app-button creation, focused-name label updates, and node-id footer creation
  • the global UI composition anchors declared in ui_common.h (main_screen, menu_g, app_g) are now defined in menu_layout.cpp instead of the entrypoint
  • main.cpp now delegates menu structure creation to ui::menu_layout::init(...) and only composes layout, runtime, and screen-sleep shells together

This removes the last large block of menu widget-tree construction from main.cpp and further concentrates reusable UI shell code under src/ui/*.

Forty-eighth concrete migration landed

The next app-shell cleanup extracts the last startup wiring helpers out of main.cpp:

  • src/app/platform_runtime.h/.cpp now own the system clock-provider wiring used by core_sys
  • src/app/bootstrap.h/.cpp now also own LVGL/display bring-up, so main.cpp no longer includes board-specific headers or calls beginLvglHelper(...) directly
  • src/app/ui_runtime.h/.cpp now own boot-overlay preparation, menu/runtime hook wiring, menu shell initialization, screen-sleep runtime startup, and wake-from-sleep post-init handling
  • src/app/bootstrap.h/.cpp and src/app/ui_runtime.h/.cpp now also hide the direct AppContext singleton plumbing from the entrypoint
  • main.cpp is now reduced to startup logging plus high-level composition of bootstrap and UI shell runtime

This makes the entrypoint much closer to a real composition root and further concentrates ESP-specific startup policy into reusable shell units.

Forty-ninth concrete migration landed

The next app-shell cleanup extracts the active main loop runtime out of main.cpp:

  • src/app/loop_runtime.h/.cpp now own USB-active loop handling, LVGL cadence, optional timing diagnostics, power-button polling, and AppContext ticking
  • main.cpp::loop() is now only a thin call into app::loop_runtime::tick()
  • runtime-local LVGL timing state now lives with the loop shell instead of the startup composition root

This removes the last behavior-heavy runtime block from main.cpp and leaves the entrypoint as a much cleaner assembly layer for future apps/* targets.

Fiftieth concrete migration landed

The repository now starts using the planned apps/* and platform/* roots for active runtime code instead of only documenting them:

  • apps/esp_pio is now a real PlatformIO application shell with its own startup_runtime and loop_runtime
  • platform/esp/arduino_common is now a real shared Arduino-on-ESP platform shell with startup-support and display-runtime code
  • src/main.cpp now delegates directly into apps/esp_pio/* instead of pulling startup/loop shells from src/app
  • platformio.ini and src/CMakeLists.txt are now wired to compile the new apps/ and platform/ runtime roots

This is the first concrete step where the new repository structure is no longer only aspirational: the active runtime shell has started to move out of src/.

Fifty-first concrete migration landed

The ESP Arduino power-policy shell no longer lives under src/app:

  • battery_guard.* moved into platform/esp/arduino_common
  • power_tier.* moved into platform/esp/arduino_common
  • GPS and UI runtime code now includes the platform power-policy surface instead of src/app/*

This keeps low-battery degradation and board power-tier reads with the active ESP runtime shell instead of leaving them in the legacy app bucket.

Fifty-second concrete migration landed

The Arduino ESP board bootstrap is no longer routed through one catch-all legacy bridge:

  • board bring-up, display initialization, and board-specific AppContext::init(...) wiring moved into platform/esp/boards/*
  • platform/esp/boards now has a real shared board-runtime surface with per-board implementations for T-Deck, T-Watch S3, and T-LoRa Pager
  • src/platform_bridge/esp_arduino_legacy_bridge.* is reduced to the remaining temporary AppContext / menu-layout accessors

This shrinks the old bridge layer and moves board-instance ownership closer to the platform board shells where it belongs.

Fifty-third concrete migration landed

The repository now has the first real independent ESP-IDF Tab5 application entry:

  • apps/esp_idf_tab5 now contains its own startup_runtime plus an app_main() entrypoint
  • the IDF-side src/CMakeLists.txt now pulls apps/esp_idf_tab5/src/* instead of the PlatformIO shell sources
  • the legacy Arduino src/main.cpp and Arduino-only legacy bridge are now excluded from the IDF component source list

This starts making apps/esp_idf_tab5 a genuine shell root instead of a documentation-only placeholder.

Fifty-fourth concrete migration landed

The remaining Arduino PlatformIO AppContext bridge logic is no longer parked under src/platform_bridge:

  • apps/esp_pio now owns initializeAppContext(), menu-layout initialization, and runtime AppContext ticking access through a dedicated app_runtime_access helper
  • platform/esp/arduino_common/startup_support is now reduced to platform startup concerns only (clock providers, wakeup cause, board bring-up)
  • the temporary src/platform_bridge layer can now be retired from the active PlatformIO shell path

This makes the PlatformIO app shell more self-contained and removes one more legacy cross-layer stopgap from the hot path.

Fifty-fifth concrete migration landed

The ESP-IDF platform root is no longer empty documentation scaffolding:

  • platform/esp/idf_common now contains real shared startup code
  • apps/esp_idf_tab5/startup_runtime now consumes platform/esp/idf_common::startup_support
  • the IDF-side app shell now has a distinct apps/* -> platform/* dependency shape rather than a standalone placeholder file

This is still an early shell, but it is now a real participant in the planned repository structure rather than only a target directory name.

Fifty-sixth concrete migration landed

The LoRa task runtime is no longer owned by the legacy src/app bucket:

  • app_tasks.* moved into platform/esp/arduino_common
  • AppContext now stops at mesh-layer assembly and no longer starts FreeRTOS radio tasks directly
  • apps/esp_pio/app_runtime_access now starts the LoRa runtime after AppContext initialization succeeds
  • shared UI / walkie features now include the platform runtime surface instead of src/app/app_tasks.h

This leaves src/app focused on application assembly state (AppContext, AppConfig) and moves one more ESP runtime concern into the planned platform/* shell.

Fifty-seventh concrete migration landed

AppContext now owns less ESP-specific runtime wiring directly:

  • BLE manager creation and the persisted BLE-enabled startup policy moved out of AppContext::init(...) into apps/esp_pio/app_runtime_access
  • ESP node-id / MAC derivation moved behind a platform helper in platform/esp/arduino_common
  • Team runtime / GPS track source / ESP-NOW pairing transport construction moved behind a platform bundle helper in platform/esp/arduino_common
  • AppContext now stores Team runtime dependencies via their abstract interfaces instead of concrete Arduino/GPS implementation classes

This further separates application assembly state from ESP shell policy and keeps src/app closer to cross-platform composition code.

Fifty-eighth concrete migration landed

The application runtime frame is now split more cleanly between application state and shell ticking:

  • AppContext::update() has been split into updateCoreServices() and dispatchPendingEvents(...)
  • BLE manager ticking and UI controller ticking moved out of AppContext and into apps/esp_pio/app_runtime_access
  • apps/esp_idf_tab5 now has its own app_runtime_access and loop_runtime shell units instead of a startup banner only
  • the IDF app shell now starts its own dedicated loop task, making it a real assembly root for later board/runtime wiring

This keeps the composition root directionally consistent: src/app holds application state and orchestration, while apps/* own the active runtime frame for each target.

Fifty-ninth concrete migration landed

The UI / notification event side-effects are no longer hard-coded inside AppContext:

  • src/app/app_event_runtime.h now defines a small runtime-hook contract for shell-specific ticking and event handling
  • AppContext now exposes attachEventRuntimeHooks(...) and tickEventRuntime() while keeping core event-state updates local
  • apps/esp_pio/event_runtime now owns board haptics, toast notifications, team-page event forwarding, and chat UI event forwarding
  • apps/esp_idf_tab5/event_runtime now mirrors the same hook surface with a placeholder implementation for later board/UI bring-up

This keeps AppContext focused on application state transitions while apps/* own how each target reacts to those events at runtime.

Sixtieth concrete migration landed

The runtime event frame is now even less centralized in AppContext:

  • hostlink::bridge::on_event(...) moved out of AppContext and into target-specific event runtime hooks
  • apps/esp_pio/event_runtime now owns both hostlink event observation and UI/notification side-effects for the active Arduino shell
  • apps/esp_idf_tab5/app_runtime_access now supports binding an AppContext and driving the same core/event runtime frame once Tab5 board wiring exists
  • apps/esp_idf_tab5/event_runtime now mirrors hostlink event observation so the IDF shell has the same composition seam as PlatformIO

This moves another cross-cutting side-effect out of the legacy app bucket and makes the IDF shell a more realistic assembly root instead of only a loop placeholder.

Sixty-first concrete migration landed

The Tab5 shell is no longer only a documented intention:

  • platform/esp/boards/tab5 now contains a concrete extracted board profile based on .tmp/M5Tab5-GPS
  • platform/esp/boards now exposes tryResolveAppContextInitHandles(...), allowing shells to probe whether a board adapter can really bind AppContext
  • apps/esp_idf_tab5 now boots through platform::esp::boards, attempts handle resolution, and is ready to call bindAppContext(...) once a real Tab5 adapter lands
  • the IDF CMake path now includes platform/esp/boards so the board runtime surface participates in the target build graph

This makes the Tab5 workstream concrete: the pin map and startup seam now exist in-repo, even though the full BoardBase adapter is still the next step.

Sixty-second concrete migration landed

The board-runtime handle seam is now safe to probe even when a target is still incomplete:

  • AppContextInitHandles now stores BoardBase* instead of a mandatory reference, removing the null-reference placeholder hack from the IDF Tab5 shell
  • apps/esp_pio and apps/esp_idf_tab5 now validate resolved board handles explicitly before calling AppContext::init(...)
  • platform/esp/boards/tab5 now logs extracted capability flags together with the pin profile derived from .tmp/M5Tab5-GPS
  • the Tab5 board profile now records board capabilities in one place so later adapters can consume the same source of truth

This keeps the new apps/* -> platform/* seam honest: shells can now probe for a board adapter without manufacturing fake references, while Tab5-specific facts continue to move out of the app layer.

Sixty-third concrete migration landed

The ESP-IDF Tab5 shell no longer drags the legacy Arduino-heavy src/ tree into its build graph:

  • apps/esp_idf_tab5/app_runtime_access now probes board/runtime availability without hard-binding to AppContext
  • apps/esp_idf_tab5/event_runtime is now a no-op seam instead of pulling legacy hostlink/UI side-effects into the IDF target
  • the IDF component source list now builds only modules/* plus apps/esp_idf_tab5 and platform/esp/*
  • src/CMakeLists.txt now explicitly treats the remaining src/ tree as out-of-scope for the current IDF target until those pieces are migrated

This is an important boundary correction: the IDF target can now advance as a real apps/* -> platform/* -> modules/* consumer instead of inheriting the unfinished Arduino shell by accident.

Sixty-fourth concrete migration landed

The ESP-IDF Tab5 target now avoids the Arduino-only radio-protocol implementation layer inside core_chat:

  • the IDF build graph now excludes modules/core_chat/src/infra/meshcore/*
  • the IDF build graph now excludes modules/core_chat/src/infra/meshtastic/*
  • generated Meshtastic protobuf transport sources under modules/core_chat/generated/meshtastic/* are also left out of the current IDF target
  • this leaves the IDF target on shared domain/usecase code while protocol-specific transport and crypto backends are migrated behind cleaner seams

This is intentionally temporary, but directionally correct: protocol implementations that still assume Arduino crypto/runtime facilities should not leak into the new IDF shell by default.

Sixty-fifth concrete migration landed

The active ESP-IDF component root has now moved out of the legacy src/ directory:

  • the top-level CMakeLists.txt now points EXTRA_COMPONENT_DIRS at apps/esp_idf_tab5
  • apps/esp_idf_tab5/CMakeLists.txt now owns the IDF build graph for the Tab5 target
  • src/CMakeLists.txt is no longer the active IDF component root
  • this removes the awkward source component src behavior and aligns the build layout with the planned apps/* -> platform/* -> modules/* structure

This is a structural migration step, not just a build workaround: the IDF target now assembles from its own app root instead of borrowing the legacy source tree's directory identity.

Sixty-sixth concrete migration landed

The ESP-IDF Tab5 component now registers migrated external sources through target_sources() instead of listing them directly in idf_component_register():

  • apps/esp_idf_tab5 now owns a tiny local stub source for IDF component registration
  • migrated module/platform sources are attached afterwards via target_sources(${COMPONENT_LIB} ...)
  • this avoids the fragile IDF source-component inference path that was tripping over external */src/* source trees

This keeps the new app root in control while we are still in the intermediate state where many migrated libraries have not yet become standalone native IDF components.

Sixty-seventh concrete migration landed

The shared ESP board-runtime switch now resolves the IDF Tab5 board implementation correctly:

  • platform/esp/boards/src/board_runtime.cpp now includes sdkconfig.h when building under ESP-IDF
  • the CONFIG_IDF_TARGET_ESP32P4 check now actually activates the Tab5 board runtime instead of falling through to the Arduino pager implementation

This is a small but important migration fix: without the IDF target config header, the shared board-runtime layer could not reliably select the right board shell under ESP-IDF.

Sixty-eighth concrete migration landed

The app-shell boundary is now slightly cleaner and no longer forces the ESP-IDF Tab5 target to carry the entire legacy src/ include root:

  • a shared AppEventRuntimeHooks contract now lives under apps/include/apps/app_event_runtime.h for the new app shells
  • apps/esp_pio and apps/esp_idf_tab5 now consume that shared app-shell header instead of including src/app/app_event_runtime.h directly
  • the ESP-IDF Tab5 component no longer adds ${CMAKE_SOURCE_DIR}/src as a blanket include directory
  • the legacy src/app/app_event_runtime.h header remains in place with the same include guard so old and new shells can coexist during migration
  • both pio run -e tlora_pager_sx1262 and idf.py build now pass after this boundary reduction

This is still an intermediate state, but it is directionally correct: shared shell contracts start moving under apps/, while the new IDF target gets less accidental visibility into the legacy tree.

Sixty-ninth concrete migration landed

The app event-runtime contract is now a real shared module surface instead of a duplicated app/legacy header pair:

  • AppEventRuntimeHooks now has a single canonical home at modules/core_sys/include/app/app_event_runtime.h
  • src/app/app_context.h, apps/esp_pio, and apps/esp_idf_tab5 now all include that shared module header directly
  • the temporary apps/include/apps/app_event_runtime.h copy has been removed
  • the legacy compatibility header src/app/app_event_runtime.h has been removed as well
  • the temporary apps/include include-path wiring has been dropped from both the PlatformIO and ESP-IDF shells because it is no longer needed for this contract
  • both pio run -e tlora_pager_sx1262 and idf.py build still pass after deleting the legacy entry point

This is the cleaner end-state for this seam: a shared shell contract now lives in modules/*, and the migration no longer depends on keeping a duplicated header alive under src/app.

Seventieth concrete migration landed

AppConfig is no longer anchored in the legacy src/app tree as an inline data-and-persistence monolith:

  • the shared configuration data model now lives at modules/core_sys/include/app/app_config.h
  • AppConfig persistence against Arduino Preferences moved into the legacy shell adapter src/app/app_config_store.*
  • src/app/app_context and src/ble/ble_manager.h now include the shared app/app_config.h module header instead of a local legacy header
  • the old src/app/app_config.h header has been removed
  • core_sys now declares its public header dependency on core_chat and core_gps, because AppConfig exposes mesh and motion types from those modules
  • AppContext::saveConfig() is now implemented in app_context.cpp, so app_context.h no longer needs to inline the persistence adapter call
  • both pio run -e tlora_pager_sx1262 and idf.py build pass after this split

This is a meaningful step toward the target structure: shared configuration state now lives in modules/*, while src/app keeps only the Arduino-specific persistence adapter that still belongs to the legacy shell.

Seventy-first concrete migration landed

The remaining src/app boundary was tightened further without pretending that the legacy application shell is already a shared module:

  • AppContext no longer stores Arduino Preferences by value in its header; it now keeps a std::unique_ptr<Preferences> and moves that concrete dependency into app_context.cpp
  • the Arduino Preferences persistence adapter for AppConfig has moved out of src/app and now lives under platform/esp/arduino_common/app_config_store.*
  • legacy config migration fallback for old region keys now also lives inside that Arduino persistence adapter instead of leaking into app_context.cpp
  • src/app/app_context.h still includes the public service/interface headers that its API exposes, but it no longer drags in Preferences.h just to declare the shell singleton
  • src/ble/ble_manager.h no longer includes app/app_config.h because it only needs chat::MeshProtocol in its public interface
  • once the header pollution was removed, a couple of UI files (menu_layout.cpp, ui_chat.cpp) were updated to include Arduino.h explicitly instead of depending on transitive includes
  • both pio run -e tlora_pager_sx1262 and idf.py build still pass after the cleanup

This finishes the current class of cleanup around src/app: the small shared contracts and config model are out, the Arduino persistence adapter is pushed down to the platform layer, and what remains in src/app is the legacy application assembly object itself.

Seventy-second concrete migration landed

  • AppContext now implements shared facade interfaces from modules/core_sys/include/app/app_facades.h.
  • the facade split now covers both high-level configuration application (switchMeshProtocol) and transport access (getMeshAdapter) so service code can stay on narrow interfaces.
  • Callers can depend on IAppConfigFacade / IAppMessagingFacade / IAppTeamFacade / IAppRuntimeFacade instead of the full AppContext header.
  • src/app/app_facade_access.h provides narrow accessors so runtime and service code can stop including src/app/app_context.h unless they really need lifecycle/init APIs.
  • hostlink/*, apps/esp_pio/src/event_runtime.cpp, src/ui/menu_runtime.cpp, and src/walkie/walkie_service.cpp now use facade access instead of reaching into the singleton directly for simple read/write operations.

Seventy-third concrete migration landed

  • UI aggregate layers now use facade access where they only need config, messaging, team, or runtime state (ui_status, ui_team, ui_gps, ui_chat, ui_contacts, ui_usb, ui_controller).
  • IAppRuntimeFacade now exposes isBleEnabled() so top-level status UI no longer depends on BleManager or the full AppContext header for a simple state query.
  • Board-specific pages that still need legacy board handles remain outside the facade for now; they will move later behind platform/runtime adapters instead of bloating AppContext again.
  • Both pio run -e tlora_pager_sx1262 and idf.py build still pass after this UI-layer cleanup.

Seventy-fourth concrete migration landed

  • Several screen component files now read config, self identity, and contact names through facade access instead of directly including app_context.h (chat_conversation_components, chat_message_list_components_watch, gps_page_components, gps_page_map, gps_route_overlay, node_info_page_components).
  • This pushes the facade split one level deeper than the top-level ui_* aggregators and exposes more hidden include dependencies at the actual screen/component boundary.

Seventy-fifth concrete migration landed

  • Large page modules now depend on IAppFacade rather than the concrete AppContext singleton where they need a mix of config, messaging, team, and maintenance operations (settings_page_components, contacts_page_components, contacts_page_layout, team_page_components, tracker_page_components).

  • A small IAppAdminFacade was added for application-level maintenance actions (broadcastNodeInfo, clearNodeDb, clearMessageDb) so settings flows can stop depending on AppContext just for those operations.

  • team_page_input.cpp no longer includes app_context.h at all because it never needed it.

  • menu_layout now depends only on IAppMessagingFacade, so the menu shell can display the node ID without pulling in the concrete AppContext type.

Seventy-sixth concrete migration landed

  • ui_energy_sweep no longer pulls AppContext just to access LoraBoard and radio task pause/resume. That responsibility moved into platform/esp/arduino_common/exclusive_lora_runtime.*.
  • The energy sweep UI now combines configFacade() with a dedicated platform radio session adapter: configuration still comes from the app layer, while exclusive hardware ownership comes from the platform layer.
  • ui_walkie_talkie dropped its direct TLoRaPagerBoard precheck and now relies on walkie::start() / walkie::get_last_error() for readiness reporting, removing another unnecessary concrete board dependency from the UI layer.

Seventy-seventh concrete migration landed

  • src/walkie/walkie_service.cpp no longer depends on TLoRaPagerBoard::getInstance() or direct board-member access for radio/codec control.
  • A dedicated Arduino platform adapter now lives at platform/esp/arduino_common/walkie_runtime.*, mirroring the earlier exclusive_lora_runtime pattern but for the walkie-talkie FSK/audio session.
  • The new walkie runtime owns acquisition of the board handle, radio-task pause/resume coordination, FSK reconfiguration, LoRa restore, and codec I/O/control entry points.
  • walkie_service now stays focused on session/state-machine logic, packetization, CODEC2 handling, and UI-facing status/error flow instead of reaching into board internals.
  • After this step, the remaining direct AppContext singleton usage under src/ui, src/walkie, and apps/esp_pio is reduced to the app-shell assembly point in apps/esp_pio/src/app_runtime_access.cpp.
  • pio run -e tlora_pager_sx1262 passes after the walkie runtime split, and the touched files are formatted with the CI clang-format rules.

Seventy-eighth concrete migration landed

  • AppEventRuntimeHooks now targets IAppFacade instead of the concrete AppContext, so app-shell event/tick handlers no longer need the legacy application singleton type in their public contract.
  • apps/esp_pio/src/event_runtime.cpp now works directly against the shared facade surface (getBoard, getBleManager, getUiController, getContactService) and drops its concrete AppContext dependency.
  • A small IAppLifecycleFacade was added so the main loop can call updateCoreServices, tickEventRuntime, and dispatchPendingEvents through a narrow interface.
  • The PlatformIO shell now pushes concrete AppContext usage back into a startup-only binding file: apps/esp_pio/src/app_context_binding.cpp owns the singleton/bootstrap path, while apps/esp_pio/src/app_runtime_access.cpp stays on facade-level runtime calls.

Seventy-ninth concrete migration landed

  • A BLE-specific facade seam now exists in modules/core_sys/include/app/app_facades.h: IAppBleFacade extends the shared app facade with the few extra hooks BLE transport still needs (getNodeStore, resetMeshConfig).
  • src/ble/ble_manager.h, src/ble/meshtastic_ble.h, and src/ble/meshcore_ble.h no longer expose AppContext in their public API; they now depend on IAppBleFacade instead.
  • This keeps concrete AppContext usage in the startup/binding path while allowing the BLE transport layer to compile against a named interface rather than the legacy singleton type.

Eightieth concrete migration landed

  • src/chat/infra/meshtastic/mt_adapter.cpp no longer reaches into AppContext::getInstance() just to read AppConfig; it now depends on configFacade() plus the shared app/app_config.h contract.
  • This removes one more singleton-style legacy access outside the app shell and keeps the Meshtastic transport closer to the same facade direction already used across UI and runtime code.

Eighty-first concrete migration landed

  • AppContext no longer directly constructs the Arduino mesh runtime router or the event-bus chat observer bridge; both now arrive through AppContextPlatformBindings factories.
  • the IMeshAdapter contract now exposes optional backend-host hooks (installBackend, backendProtocol, backendForProtocol), so src/app and src/ble stop depending on MeshAdapterRouter concrete casts.
  • platform/esp/arduino_common remains the place that creates MeshAdapterRouter and ChatEventBusBridge, while apps/esp_pio injects that platform bundle during startup.

This further shrinks src/app toward an application shell/facade layer and keeps runtime/platform concrete chat wiring inside platform/* plus apps/*.

Eighty-second concrete migration landed

  • AppContext no longer owns a Preferences instance and no longer directly includes platform/esp/arduino_common/app_config_store.h.
  • platform config load/save plus message-tone volume lookup now come through AppContextPlatformBindings, with Arduino implementations provided by platform/esp/arduino_common/app_config_store.
  • this keeps NVS/Preferences details inside the platform layer while src/app only consumes injected configuration persistence hooks.

This is another small but important step toward making src/app a platform-neutral shell instead of a place where ESP persistence APIs leak upward.

Eighty-third concrete migration landed

  • AppContext no longer directly constructs team::infra::TeamCrypto, TeamEventBusSink, TeamAppDataEventBusBridge, or TeamPairingEventBusSink; those now come through AppContextPlatformBindings factories.
  • GPS runtime initialization, position-config apply, track-recorder setup, and team-mode propagation now also flow through platform bindings instead of calling GpsService / TrackRecorder directly from src/app.
  • AppContext now stores team collaborators mostly behind interface types (ITeamCrypto, ITeamEventSink, ITeamPairingEventSink, UnhandledAppDataObserver) rather than ESP/event-bus concrete classes.

This keeps more runtime concrete wiring inside platform/esp/arduino_common and further reduces src/app to an application-shell orchestration layer.

Eighty-fourth concrete migration landed

  • AppContext::dispatchPendingEvents() no longer hard-codes chat/contact core event handling; it now acts as a thin event-bus pump that delegates to AppEventRuntimeHooks.
  • AppEventRuntimeHooks gained a dispatch_event stage so app-shell runtimes can consume core state-update events before UI/event presentation logic runs.
  • apps/esp_pio/src/event_runtime.cpp now owns handling of ChatSendResult, NodeInfoUpdate, NodeProtocolUpdate, and NodePositionUpdate, keeping the legacy app shell thinner and pushing event policy outward toward apps/*.
  • This keeps src/app closer to orchestration/facade responsibilities while preserving the existing UI/runtime behavior.

Eighty-fifth concrete migration landed

  • The PlatformIO app shell no longer owns the concrete BLE startup restore path or the concrete event-runtime hook implementation.
  • Those responsibilities now live under platform/esp/arduino_common/app_runtime_support.*, alongside the rest of the Arduino-on-ESP runtime glue.
  • apps/esp_pio/src/app_context_binding.cpp is thinner again: it now wires AppContext using platform-provided helpers instead of directly constructing BleManager or owning the event hook code.
  • This keeps apps/* closer to pure composition while moving one more block of ESP/Arduino runtime behavior into platform/*.

Eighty-sixth concrete migration landed

  • AppContext::updateCoreServices() no longer hard-codes hostlink/chat/team runtime orchestration.
  • AppEventRuntimeHooks now also carries an update_core_services stage, so app/platform runtime code can own service-update policy alongside tick and event handling.
  • The Arduino ESP runtime helper now owns hostlink::process_pending_commands(), chat/team incoming processing, team-mode propagation, and team track-sampler updates.
  • This trims another chunk of orchestration logic out of src/app and keeps AppContext focused on facade state plus injected collaborators.

Eighty-seventh concrete migration landed

  • The old scattered team/contact factories in AppContextPlatformBindings have been consolidated into higher-level TeamServicesBundle and ContactServicesBundle factories.
  • AppContext::initTeamServices() and AppContext::initContactServices() now mostly consume preassembled bundles instead of directly constructing service graphs.
  • The Arduino ESP platform layer now assembles TeamService, TeamController, TeamTrackSampler, ContactService, and the related storage/runtime collaborators on behalf of the app shell.
  • This reduces constructor/orchestration noise in src/app and moves another slice of concrete startup wiring into platform/*.

Eighty-eighth concrete migration landed

  • Startup-finalization logic now has its own finalize_startup binding stage in AppContextPlatformBindings.
  • Team key restore, UI timezone warm-up, and initial team-mode propagation moved out of AppContext::init() and into the Arduino ESP startup finalizer.
  • The legacy AppContext::restoreTeamKeys() helper has been removed, along with the related UI-store/timezone includes from src/app/app_context.cpp.
  • This trims another non-core startup slice from src/app and keeps startup-side runtime policy in the platform/app shell boundary.

Eighty-ninth concrete migration landed

  • Chat startup assembly is now bundle-based in the same style as the earlier team/contact migration.
  • AppContextPlatformBindings now exposes a higher-level ChatServicesBundle factory instead of separate chat-store / mesh-runtime / message-observer factories.
  • AppContext no longer manually constructs ChatModel, RamStore, MeshAdapterRouter, or ChatService; it now consumes a preassembled chat bundle and only applies runtime configuration policy afterwards.
  • This further shrinks src/app toward orchestration/facade duties while moving concrete chat graph assembly into platform/*.

Ninetieth concrete migration landed

  • src/hostlink no longer depends on the temporary local hostlink_codec.h / hostlink_types.h wrapper headers.
  • The remaining runtime-side hostlink sources now include canonical modules/core_hostlink headers directly.
  • The two wrapper headers have been removed, shrinking another small piece of the legacy src/hostlink surface.

Ninety-first concrete migration landed

  • src/app/app_facade_access no longer hard-binds itself to AppContext::getInstance().
  • The facade access layer now works through an explicit bindAppFacade() / unbindAppFacade() registration step over IAppFacade.
  • apps/esp_pio now binds the concrete facade after successful app-context initialization, which makes the access layer ready for future non-AppContext shells.

Ninety-second concrete migration landed

  • app_facade_access.* has moved out of src/app and into modules/core_sys now that it only depends on shared facade interfaces.
  • UI, hostlink, walkie, and app-shell callers now include canonical app/app_facade_access.h instead of climbing back into src/app with relative paths.
  • The legacy src/app/app_facade_access.* copy has been removed, further shrinking the old app-shell directory.

Ninety-third concrete migration landed

  • AppContextPlatformBindings has moved out of src/app into modules/core_sys as a shared app/platform contract.
  • The concrete AppContext implementation now lives under apps/esp_pio, matching its current role as a PlatformIO app-shell composition root.
  • src/app is now cleared of concrete sources, which sharpens the repository boundary between shared modules, platform glue, and the active app shell.

Ninety-fourth concrete migration landed

  • The remaining ESP/Arduino hostlink runtime shell (hostlink_service, hostlink_bridge_radio, and hostlink_config_service) has moved out of src/hostlink and into platform/esp/arduino_common.
  • Callers now include canonical platform hostlink headers under platform/esp/arduino_common/hostlink/*.
  • The protocol reference document moved alongside the shared hostlink module under modules/core_hostlink/hostlink_protocol.md.

Ninety-fifth concrete migration landed

  • The remaining ESP/Arduino GPS runtime shell (gps_service, track_recorder, gps_service_api, gps_hw_status, GPS, and the HAL-backed GPS adapters) has moved out of src/gps and into platform/esp/arduino_common.
  • Callers now include canonical platform GPS runtime headers under platform/esp/arduino_common/gps/* and canonical shared GPS headers under gps/....
  • The temporary src/gps shim headers have been removed, which eliminates another legacy include surface from the repository.

Ninety-sixth concrete migration landed

  • The remaining ESP/Arduino team runtime adapters (team_crypto, event-bus bridges/sinks, ESP-NOW pairing transport/service shell, Arduino runtime adapter, and GPS-backed team track source) have moved out of src/team/infra and into platform/esp/arduino_common.
  • Callers now include canonical shared team headers from team/... and canonical platform team runtime headers from platform/esp/arduino_common/team/....
  • The temporary src/team shim headers have been removed, eliminating another legacy include surface from the repository.

Ninety-seventh concrete migration landed

  • The remaining ESP/Arduino HAL wrappers (hal_gps and hal_motion) have moved out of src/hal and into platform/esp/arduino_common.
  • Callers still include the stable hal/... prefix, but that prefix is now served by the platform layer instead of the legacy src tree.
  • This removes another platform-owned implementation pocket from src without changing higher-level GPS service contracts.

Ninety-eighth concrete migration landed

  • The shared ESP board contracts and concrete board implementations have moved out of src/board and into platform/esp/boards.
  • The canonical board/... include prefix is preserved, so callers no longer depend on the legacy source-tree location while board ownership is now explicit in the platform layer.
  • Board implementation .cpp files are now built from the esp_boards library, with environment guards keeping non-target boards from hard-failing unrelated builds.

Ninety-ninth concrete migration landed

  • The remaining ESP display abstractions and panel drivers (DisplayInterface, DisplayConfig, BrightnessController, and the ST77xx drivers) have moved out of src/display and into platform/esp/boards.
  • Board-local rotary input has moved with them into the same board platform layer.
  • Callers still use stable display/... and input/rotary/... include prefixes, but those prefixes are now served by the explicit board platform library instead of the legacy src tree.

One hundredth concrete migration landed

  • The remaining ESP/Arduino BLE runtime, USB CDC transport, and Morse input runtime have moved out of src/ble, src/usb, and src/input into platform/esp/arduino_common.
  • Callers now use stable ble/..., usb/..., and input/... include prefixes backed by the platform layer rather than the legacy src tree.
  • This removes another large runtime/framework-dependent surface from src while preserving the existing public include names.

One hundred first concrete migration landed

  • The remaining ESP/Arduino walkie-talkie runtime shell has moved out of src/walkie and into platform/esp/arduino_common.
  • Callers keep using the stable walkie/... include prefix, but that prefix is now owned by the platform layer instead of the legacy src tree.
  • This keeps push-to-talk runtime behavior in the Arduino platform shell while further shrinking the old monolithic source layout.

One hundred second concrete migration landed

  • The remaining ESP/Arduino SSTV runtime and DSP helpers have moved out of src/sstv, and screen_sleep.* has moved out of src/, into platform/esp/arduino_common.
  • Callers keep using stable sstv/... and screen_sleep.h include names, but those headers are now served by the platform layer.
  • This removes another runtime-heavy feature slice from the legacy src tree and makes the platform/esp/arduino_common boundary more complete.

One hundred third concrete migration landed

  • The remaining ESP audio codec implementation tree has moved out of src/audio/codec into a dedicated platform library at platform/esp/arduino_audio_codec.
  • Callers keep using the stable audio/codec/... include prefix, but that prefix is now owned by an explicit platform package instead of the legacy src tree.
  • This removes the last non-UI/non-sys platform-heavy pocket from src and gives the codec stack a cleaner component boundary than folding it into the generic runtime glue library.

One hundred fourth concrete migration landed

  • The FreeRTOS-backed event bus has moved out of src/sys and into platform/esp/arduino_common under the stable sys/event_bus.h include prefix.
  • The temporary src/sys/clock.h and src/sys/ringbuf.h compatibility shims have been removed, so sys/clock.h and sys/ringbuf.h now resolve directly from modules/core_sys.
  • After this step, the legacy src/sys directory is fully retired from the active tree.

One hundred fifth concrete migration landed

  • The shared AppScreen contract has moved out of src/ui into modules/ui_shared/include/ui/app_screen.h.
  • Shared UI runtime primitives (set_default_group, app enter/exit switching, delayed return-to-menu flow, and generic create_menu) now live in modules/ui_shared/include/ui/app_runtime.h and modules/ui_shared/src/ui/app_runtime.cpp.
  • src/ui/ui_common.* now keeps only product/platform-tinted UI helpers such as battery formatting, timezone persistence, coordinate formatting, and screenshot handling.

One hundred sixth concrete migration landed

  • Pure UI formatters (ui_format_battery and ui_format_coords) have moved out of src/ui/ui_common.cpp into modules/ui_shared/include/ui/formatters.h and modules/ui_shared/src/ui/formatters.cpp.
  • src/ui/ui_common.* now focuses more narrowly on board/product-tinted helpers such as top-bar battery reads, timezone persistence, and screenshot-to-SD behavior.
  • This keeps LVGL-dependent but platform-neutral formatting logic in the shared UI module instead of the app-local UI shell.

One hundred seventh concrete migration landed

  • The main menu shell (menu_layout.* and menu_runtime.*) has moved out of src/ui and into apps/esp_pio, matching its role as PlatformIO app-shell composition code.
  • The current app-screen registry has also moved into apps/esp_pio as a private implementation detail instead of staying under the legacy shared-looking src/ui tree.
  • After this step, the active src/ui tree is narrower: screen implementations stay there for now, while top-level menu shell composition belongs to the app shell.

One hundred eighth concrete migration landed

  • ui_boot.* and watch_face.* have moved out of src/ui and into apps/esp_pio as private app-shell UI helpers.
  • These files are only used by the active PlatformIO shell startup/menu flow, so keeping them in apps/esp_pio is cleaner than leaving them in the legacy shared-looking UI tree.
  • This further narrows src/ui toward reusable screen implementations plus still-unmigrated shared/product UI helpers.

One hundred ninth concrete migration landed

  • The menu-status shell (ui_status.*) and the app-shell-only top-level page-wrapper files have moved out of src/ui and into apps/esp_pio/src.
  • This includes wrappers such as contacts / GPS / chat / tracker / SSTV / USB / walkie-talkie entrypoints that are only used by the current PlatformIO app registry and loop shell.
  • After this step, src/ui is more clearly centered on reusable screen implementations, screen helpers, and the remaining shared/product UI runtime pieces that still need a better home.

One hundred tenth concrete migration landed

  • Moved apps/esp_pio-owned ui_controller.* and ui_team.* out of src/ui into apps/esp_pio/src and rewired include paths to ui/... shared headers.
  • Introduced modules/ui_shared/include/ui/chat_ui_runtime.h so AppContext, team screen code, and runtime hooks depend on a small chat UI facade instead of the concrete UiController type.
  • Split ESP Arduino runtime event wiring into platform/esp/arduino_common core hooks plus apps/esp_pio/src/event_runtime.cpp so platform code no longer depends directly on app-shell chat/team UI files.

One hundred eleventh concrete migration landed

  • Moved platform-bound UI glue ui_common.* and LV_Helper* out of src/ui into platform/esp/arduino_common, keeping the public include surface on canonical ui/... paths.
  • Moved shared palette constants ui_theme.h into modules/ui_shared/include/ui/ui_theme.h and rewired remaining screens/widgets to include the shared header.
  • Deleted dead src/ui/compose_flow.h; it had no implementation and no callers.

One hundred twelfth concrete migration landed

  • Moved shared widgets top_bar, system_notification, and toast_widget from src/ui/widgets into modules/ui_shared, and rewired callers onto canonical ui/widgets/... includes.
  • This leaves only device-specific or still-unmigrated widgets under src/ui/widgets (ble_pairing_popup, ime, map).

One hundred thirteenth concrete migration landed

  • Moved shared Bluetooth pairing popup and IME widgets (ble_pairing_popup, ime_widget, pinyin_ime, pinyin_data) from src/ui/widgets into modules/ui_shared.
  • Rewired all callers onto canonical ui/widgets/... includes so app shell, platform BLE glue, and legacy screens no longer depend on src/ui/widgets paths for those shared widgets.
  • Left src/ui/widgets/map/* in place for now because it is still tightly coupled to GPS page state, LVGL image decoding, SD-backed tile storage, and ESP display/SPI coordination.

One hundred fourteenth concrete migration landed

  • Moved map_tiles from src/ui/widgets/map/* into platform/esp/arduino_common/ui/widgets/map/* because it depends on ESP display SPI locking, FreeRTOS timing, Arduino/SD storage, and LVGL-backed tile decode/runtime behavior.
  • Kept the public include surface stable at ui/widgets/map/map_tiles.h, so GPS and node-info screens no longer rely on a legacy src/ui/widgets physical path.
  • This clears the remaining files under src/ui/widgets; old widget-layer code now lives either in modules/ui_shared or platform/esp/arduino_common.

Next IDF convergence note

The repository now has evidence of a second real ESP-IDF board target outside Tab5:

  • .tmp/T-Display-P4 is a pure ESP-IDF device reference tree for an esp32p4 target
  • this means apps/esp_idf_tab5 should be treated as a transitional shell, not the final IDF app-layer shape
  • the intended end-state is apps/esp_idf/ as the shared IDF shell root, with target descriptors under apps/esp_idf/targets/tab5 and apps/esp_idf/targets/t_display_p4
  • board-specific runtime details should continue to converge downward into boards/tab5 and boards/t_display_p4

This reframes the IDF plan from "one board-specific shell" to "one shared IDF shell with multiple board targets".

Shared IDF runtime units extracted

The first IDF app-shell code has now moved out of the Tab5-specific shell and into the shared apps/esp_idf root:

  • shared app_runtime_access, event_runtime, loop_runtime, and startup_runtime now live under apps/esp_idf
  • apps/esp_idf_tab5 now keeps a thin target-specific wrapper layer plus runtime_config and app_main() entry ownership
  • apps/esp_idf_tab5/CMakeLists.txt now pulls shared sources from apps/esp_idf/src/* while keeping the active IDF component root stable

This is the first concrete code move from a board-specific IDF shell toward a shared-IDF-shell-plus-targets layout.

One hundred fifteenth concrete migration landed

  • the top-level IDF build now selects its active target shell through TRAIL_MATE_IDF_TARGET instead of hardwiring apps/esp_idf_tab5
  • introduced apps/esp_idf_t_display_p4 as a second transitional IDF target shell so the repository no longer assumes Tab5 is the only IDF board
  • changed platform/esp/boards/src/board_runtime.cpp to select board runtime by explicit board macro (TRAIL_MATE_ESP_BOARD_*) instead of by CONFIG_IDF_TARGET_ESP32P4, which would incorrectly collapse multiple ESP32-P4 boards onto the same runtime implementation
  • added initial T-Display-P4 target metadata and board-profile placeholders under apps/esp_idf/targets/t_display_p4, now converged into boards/t_display_p4

This makes the IDF side structurally ready for multiple boards that share the same SoC family.

One hundred sixteenth concrete migration landed

  • apps/esp_idf is now the active shared ESP-IDF component root; the top-level CMakeLists.txt no longer switches between separate target-specific component directories
  • target selection now feeds SDKCONFIG_DEFAULTS from apps/esp_idf/targets/<target>/sdkconfig.defaults, so target descriptors are part of the real build path rather than documentation only
  • shared app_main() and runtime_config moved into apps/esp_idf, removing the need to duplicate wrapper shells for Tab5 and T-Display-P4
  • board runtime selection remains explicit through TRAIL_MATE_IDF_TARGET -> TRAIL_MATE_ESP_BOARD_*, which keeps multiple boards on the same SoC family cleanly separated

This is the first point where apps/esp_idf stops being only a shared helper directory and becomes the real IDF app root.

One hundred seventeenth concrete migration landed

  • Moved the reusable main-menu shell pieces back out of apps/esp_pio and into modules/ui_shared: menu_profile.*, menu_layout.*, menu_runtime.*, ui_boot.*, watch_face.*, and ui_status.*.
  • Introduced shared shell contracts (ui::AppCatalog, ui::startup_shell, and ui::loop_shell) so apps/esp_pio now mostly assembles Arduino/PlatformIO-specific hooks instead of owning the LVGL shell implementation.
  • Dropped the now-unused apps/esp_pio/include/apps/esp_pio/* compatibility headers for those menu-shell internals, keeping apps/esp_pio focused on entry/runtime composition and app-registry ownership.

This makes the pager / T-Deck / Tab5 menu stack structurally one shared shell with per-device profiles, instead of a PlatformIO-owned menu tree plus board-specific conditionals.

One hundred eighteenth concrete migration landed

  • Folded the default watch-face wiring, screen-sleep hook assembly, and startup completion path into modules/ui_shared/src/ui/startup_shell.cpp.
  • apps/esp_pio/src/startup_runtime.cpp now stays focused on Arduino bring-up order, board/display initialization, AppContext bootstrap, and handing a small hook bundle into the shared startup shell.
  • Screen-sleep wake handling is now consistently sourced from the shared menu runtime (ui::menu_runtime::onWakeFromSleep()), instead of being reassembled in the PlatformIO startup file.

This keeps the product shell startup layer thinner and makes the shared menu/watch-face/screen-sleep behavior easier to reuse from future app targets.

One hundred nineteenth concrete migration landed

  • Introduced apps/esp_idf-local startup_shell and loop_shell so the shared IDF app root now has the same thin-runtime shape as apps/esp_pio: runtime entrypoints assemble hooks, while the reusable shell flow lives behind a small contract.
  • apps/esp_idf/src/startup_runtime.cpp now only wires board bring-up, startup banner logging, AppContext bootstrap, and loop start hooks into startup_shell.
  • apps/esp_idf/src/loop_runtime.cpp now only wires app_runtime_access::tick() into loop_shell, leaving task creation, delay cadence, and error logging inside the shared IDF loop skeleton.

This keeps target-specific IDF runtime entrypoints small and makes it easier to share future startup/loop behavior across Tab5, T-Display-P4, and later IDF targets.