# Linux 适配现状评估与开发指南 评估日期:2026-05-08 适用范围:本文评估并指导 Trail Mate 仓库中的 Linux 线,重点覆盖 `apps/linux_sim`、`apps/linux_rpi`、`apps/linux_unoq`、`platform/linux/*`、 `modules/*` 与 `modules/core_sys/include/platform/ui/*` 之间的关系。 本文不是单个功能的任务清单。它的目标是回答三个问题: 1. 当前 Linux 适配到底做到哪种程度。 2. 接下来应该先做什么,后做什么。 3. 怎样优雅地适配 Linux,而不是把 Linux 变成另一个长期分叉。 ## 0. 总结 当前 Linux 适配已经从“结构成型、模拟器可验证、真实设备尚未闭环”,推进到了“真机闭环结构正在搭建,但当前工作区还没有重新 build-closed”的阶段。 更具体地说: - 仓库结构已经把 Linux 当成一等目标在推进,而不是临时实验目录。 - `apps/linux_sim` 仍然是最快的共享 UI 验证壳,并且 source list 已经开始通过 `cmake/TrailMateLinuxSources.cmake` 收口。 - `platform/linux/common` 已经实现了一批 Linux-safe 平台合约,包括 settings、time、screen、device、GPS NMEA/env、hostlink TCP、route/tracker 文件存储、pack repository、SSTV/walkie/LoRa 的模拟运行时,以及 Wi-Fi/USB/FOTA 等显式 unsupported 桩。本轮又新增了 runtime path、env helper、capability status、runtime mode、demo world 抽取等结构;最新推进中,`CapabilityStatus` 已经上移为 `platform::ui` 合约种子并进入 LoRa/SSTV/Walkie public header,hostlink/team/SSTV/map tiles 也开始更多复用 `runtime_paths`。 - `apps/linux_rpi` 仍有两条设备路径:一条是 repo-local CMake framebuffer shell,一条是 `M5Stack_Linux_Libs` SCons SDK shell。custom framebuffer path 已经接入 evdev 源文件和输入 drain,但仍没有完成 build-closed 与真机验证;SDK path 已从很薄的占位 main 推进到 fbdev 自动探测、LVGL evdev 初始化和启动日志齐备的 bring-up shell,但仍尚未接上 Trail Mate shared shell。 - 边界检查已经存在并通过。当前 `modules/ui_shared` 和 `modules/core_*` 没有命中 `Arduino.h`、`Preferences`、`freertos/*`、`platform/esp/*` 这类关键平台污染规则。 - 真机完成度仍低于模拟器完成度。显示、输入、能力模型、真实硬件运行时、SDK 主路径、安装发布、设备 CI 都还没有形成完整闭环;其中能力模型已经有合约种子和部分 public API,但还没有进入 contract inventory 和 UI 呈现。 - 本轮代码有明显架构进展,上一轮列出的主要 compile/runtime gate 已基本修掉;当前最大风险已经从“源码明显不一致”转为“尚未完整构建、测试和真机验证”。不能因为结构文件已经出现,就把 P1/P2/P3/P4/P5/P6 直接标记为完成。 一句话判断: > 现在不是“Linux 还没开始”,也不是“Linux 已经适配好了”。本轮回归后,Linux 线已经进入 L4 结构施工阶段;输入路径、SDK bring-up、路径安全和能力合约的结构明显前进,并且一部分上轮 P0 项已经从“待修”降级为“待验证”。但当前工作区仍需要先完成剩余编译验证和真机验证,才能把 L4 从“结构存在”推进到“可构建、可运行、可验证”。 ### 0.1 2026-05-07 回归结论(历史记录) 本次回归基于 `LINUX_ADAPTATION_GUIDE.md` 之后的代码改动重新评估,结论如下: 注意:本节保留 2026-05-07 当天的判断,用于观察演进;当前状态以 2026-05-08 的 `0.6` 小节为准。 | 指南项 | 本轮代码变化 | 当前判断 | | --- | --- | --- | | P1 CMake source list 收口 | 新增 `cmake/TrailMateLinuxSources.cmake`,`linux_sim`/`linux_rpi` 的 CMake 明显变薄 | 方向正确,结构基本落地;但必须通过 sim/rpi 两套构建后才能标记 done | | P2 LVGL ownership 拆分 | 新增 `ShellSession`、`CanvasLvglHost`、`NativeLvglHost` | 方向正确,已从“单一 runner”进入“双 host”结构;但当前 header/callback 细节仍会阻断或破坏输入 | | P3 真机输入 | 新增 `EvdevInput`,`LinuxFramebufferPlatform::drainInput()` 改为从 evdev 读取 | 历史判断:当日还没闭环;该行的 source wiring / key map / 目录探测问题已在 2026-05-08 回归中更新 | | P4 runtime path/env | 新增 `platform/linux/runtime_paths.*`、`env_config.*`,route/tracker 删除路径开始限制在根目录下 | 明显进展;但 settings/sstv/hostlink/team 等还没有完全统一到同一套 path helper | | P5 capability truth model | 新增 `CapabilityState`/`CapabilityStatus`,LoRa/Walkie/SSTV 开始返回 `Simulated` | 概念已经落地为代码,但还没有成为 `platform::ui::*` 合约的一部分,也没有驱动 UI 呈现 | | P6 demo world/facade 拆分 | 新增 `linux_demo_world.*` | 只是第一步:app facade 里仍保留旧 loopback mesh、dummy crypto、demo seeding,`runtime_mode` 尚未真正控制 composition | | P7 M5 SDK 主路径 | 暂未看到 shared shell 接入 `M5Stack_Linux_Libs` | 仍未完成 | | P8 第一个验证 slice | path safety smoke 新增,Settings slice 还未形成真实验收闭环 | 测试方向正确,但当前 smoke test 本身还需要 include 修正 | 当前最高优先级不是继续扩大功能面,而是先让这轮结构性改动重新达到“能构建、能跑 smoke、能解释能力真假”的状态。 ### 0.2 本轮必须先修的具体阻断点 这些问题属于“先修它们,否则后续评估会失真”的层级: | 类型 | 文件 | 2026-05-08 状态 | 仍需动作 | | --- | --- | --- | --- | | Build gate | `platform/linux/common/include/app/linux_demo_world.h` / `platform/linux/common/src/app/linux_demo_world.cpp` | 上轮 header ``、constexpr node id、重复定义风险已修 | 用完整构建确认;后续再清理 facade 内仍重复存在的 loopback mesh 逻辑 | | Architecture gate | `platform/linux/common/src/app/linux_app_facade.cpp` | demo seeding 已被 `demo_world_enabled(resolve_runtime_mode())` gate | 仍要把 loopback mesh、dummy crypto、loopback pairing 从真实设备默认 composition 中拆出去 | | Build gate | `platform/linux/common/include/ui/shell_ui_runner.h` / `platform/linux/common/src/ui/shell_ui_runner.cpp` | callback 已恢复为 `lv_area_t` / `lv_indev_data_t` 精确签名,header 只做 LVGL 类型前向声明 | 用完整构建确认 LVGL typedef 与前向声明是否兼容;如果 LVGL 的 `lv_area_t` / `lv_indev_data_t` 不是带 `_lv_*` tag 的 typedef,应改为包含 `lvgl.h` 或把 callback 降到 `.cpp` 私有自由函数 | | Runtime gate | `platform/linux/common/src/ui/shell_ui_runner.cpp` | `hasPendingKeyEvent()` 已加入,`continue_reading` 不再消费下一事件 | 需要用小测试或真机手测确认 press/release 都能被 LVGL 收到 | | Build gate | `apps/linux_rpi/CMakeLists.txt` | `evdev_input.cpp` 已加入 device executable | 用 Linux/WSL CI 构建确认 | | Build gate | `platform/linux/rpi/src/platform/device/evdev_input.cpp` | key map 已改为 `std::to_array({...})`,手写数量和 CTAD 风险已修 | 用 Linux 构建确认;后续补真机键位采样和映射测试 | | Runtime gate | `platform/linux/rpi/src/platform/device/evdev_input.cpp` | by-path/by-id 不存在目录已用 `error_code` 与 `is_directory` 防护 | 还需真机采样 Cardputer Zero 内置键盘事件码,完善 Fn/组合键映射 | | Build gate | `apps/linux_sim/tests/path_safety_smoke.cpp` | 已 include `platform/linux/capability_status.h` | 用 `ctest` 确认 smoke 真正通过 | | Contract gate | `modules/core_sys/include/platform/ui/capability_status.h` / `modules/core_sys/include/platform/ui/{lora,sstv,walkie}_runtime.h` | `CapabilityStatus` 已从 Linux helper 上移为 `platform::ui` 合约种子,Linux helper 只做 re-export | 还要在各 runtime public header 中声明 `capability_status()`,更新 contract inventory,并让 UI 使用状态而不是只看 `is_supported()` | | Path gate | `platform/linux/common/src/platform/ui/*` / `platform/linux/common/src/ui/widgets/map/map_tiles.cpp` | hostlink、team store、SSTV、map tiles 已改用 `runtime_paths`,hostlink 默认 bind 已变成 loopback | `pack_repository_runtime.cpp` 仍有 ad hoc root;map tiles 等处旧 env 常量已无实际用途;`runtime_paths.h` 仍提到 fsync,但 `.cpp` 实现已明确为 no-fsync temp+rename,需要二选一校准 | | Runtime mode | `apps/linux_sim/src/targets/simulator_main.cpp` / `platform/linux/common/include/platform/linux/runtime_mode.h` | simulator main 只在用户未设置 `TRAIL_MATE_RUNTIME_MODE` 时默认写入 `demo`;Windows `_putenv_s()` 已进入同一 guard | 继续用 Windows/WSL 启动 smoke 确认 env override;继续确认 device shell 默认不进入 demo;真实设备 composition 仍需拆掉 dummy/loopback 默认实现 | | Specification gate | `apps/linux_rpi/docs/specification/project-baseline.md` 等 | 部分文字已更新,但仍需和 build/test 事实对齐 | `DONE` 只用于 build/test/真机验证过的事实;目标态用 `TARGET`、`PARTIAL` 或 `NOT YET VALIDATED` | ### 0.3 本轮验证记录 本轮回归做了有限验证,结论要分清: | 命令 | 结果 | 解释 | | --- | --- | --- | | `python scripts/check_platform_ui_boundaries.py` | 通过 | 说明 shared/core 的 ESP/Arduino/FreeRTOS include 污染边界仍然干净 | | `cmake -S apps/linux_sim -B .codex-build/linux-sim-review -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug` | 配置通过 | 说明新的 CMake helper 至少能被配置阶段消费 | | `cmake --build .codex-build/linux-sim-review --target trailmate_cardputer_zero_path_safety_smoke -j 8` | 未能验证项目代码 | 本机 `clang++ 14` 被 VS 2022 STL 以 “expected Clang 19.0.0 or newer” 拦截,失败发生在标准库兼容层,不代表项目代码已经或没有编译通过 | | `cmake -S apps/linux_sim -B .codex-build/linux-sim-msvc -G "Visual Studio 17 2022" -A x64 -DBUILD_TESTING=ON` | 超时 | 124 秒内未完成配置,不能作为通过或失败结论 | 因此,本文中的 compile gate 清单来自代码阅读和目标依赖关系回归;最终仍要用 Linux/WSL CI 或可用的 MSVC generator 完整构建来确认。 ### 0.4 2026-05-08 回归结论 注意:本节记录上一轮 2026-05-08 回归点;当前最新判断以 `0.6` 为准。 本轮相对 2026-05-07 的回归结果如下: | 指南项 | 2026-05-08 观察 | 当前判断 | | --- | --- | --- | | P1 CMake source list 收口 | rpi target 已显式加入 `platform/linux/rpi/src/platform/device/evdev_input.cpp`,sim/rpi 仍通过 shared helper 组织 common/ui sources | P1 继续向好;剩余是完整构建验证、helper 注释/编码清理,以及 target define 语义收口 | | P2 LVGL ownership 拆分 | `ShellSession::hasPendingKeyEvent()` 已加入,`readInputCb()` 不再为了 `continue_reading` 消费下一个事件;callback 已恢复为 `lv_area_t` / `lv_indev_data_t` 精确签名 | 上一轮的输入吞事件问题已修;callback 风险已收窄到 LVGL typedef/前向声明兼容性,仍需构建确认;SDK native host 仍未接入 | | P3 真机输入 | `evdev_input.cpp` 已被 rpi CMake 编入;`/dev/input/by-path` 与 `/dev/input/by-id` 已用 `error_code` 与目录判断保护;`kKeyMap` 已改为 `std::to_array` | source wiring、目录探测、key map 初始化风险已修;剩余是 Linux 构建确认和 Cardputer Zero 真机键位采样 | | P4 runtime path/env | `path_safety_smoke.cpp` 已 include `capability_status.h`;route/tracker 仍走 root containment | path safety smoke 的 include 问题已修;但 SSTV、team store、hostlink、pack repository 还没有完全统一到 `runtime_paths` | | P5 capability truth model | `CapabilityState` 仍是 Linux 内部 helper,LoRa/Walkie/SSTV 仍通过 `capability_status()` 标记 `Simulated` | 概念稳定,但尚未进入 `platform::ui::*` 公共合约和 UI 呈现 | | P6 demo world/facade 拆分 | `runtime_mode.h` 已补 ``,`ensureStarted()` 已用 `demo_world_enabled(resolve_runtime_mode())` gate demo seeding,simulator main 显式默认 `TRAIL_MATE_RUNTIME_MODE=demo` | demo seed 默认语义更清楚;但 facade 仍总是组合 loopback mesh、dummy crypto、loopback pairing,DeviceLocal 还不是干净真实设备 composition | | P7 M5 SDK 主路径 | 未看到 `apps/linux_rpi/main/src/main.cpp` 接入 `ShellSession`/`NativeLvglHost` | 仍未完成 | | P8 第一个验证 slice | path safety smoke 源码更完整,但本地尚未完成有效构建/ctest | 测试方向正确,验收仍依赖 Linux/WSL/CI 构建 | 一句话:这轮把 2026-05-07 的一批“明确红灯”基本修成了“等待构建确认”,但还没越过 build/test/真机三道门。下一步最小闭环应是直接在 Linux/WSL 上构建 `linux_sim` tests 和 `linux_rpi` framebuffer target,然后做 Cardputer Zero 真机键位采样与导航验收。 ### 0.5 2026-05-08 最新回归结论 本轮在 `0.4` 之后继续推进了三类结构:SDK bring-up、能力合约上移、runtime path 覆盖。结论要比 `0.4` 更细:有些项已经从“缺结构”变成“缺公开 API / 缺验证”,但仍不能直接标记为完成。 | 指南项 | 最新观察 | 当前判断 | | --- | --- | --- | | P1 CMake source list 收口 | `cmake/TrailMateLinuxSources.cmake` 的路径注释已经校准为“从 helper 所在目录推 repo root”;rpi target 继续显式编入 `evdev_input.cpp` | 结构上继续健康;当前剩余不是再抽象,而是完整 Linux/WSL build、ctest 和 rpi target build | | P2 LVGL ownership 拆分 | `ShellSession` / `CanvasLvglHost` / `NativeLvglHost` 结构未再倒退;callback 精确签名和 `hasPendingKeyEvent()` 仍保持 | 方向稳定;仍需构建确认 LVGL typedef/前向声明兼容性,并把 SDK main loop 从注释切到 `NativeLvglHost` | | P3 真机输入 | CMake framebuffer path 的 evdev adapter 仍是当前 shared shell 输入主线;SDK path 也已用 LVGL evdev 创建 pointer/keypad | 输入结构明显增强;仍缺真机事件码采样、Fn/组合键映射、press/release 回归和 rpi build 结果 | | P4 runtime path/env | settings、route、tracker、hostlink、team store、SSTV 已基本走 `runtime_paths`;hostlink 默认 bind 已是 `127.0.0.1`,需要对外监听时显式设 `TRAIL_MATE_HOSTLINK_BIND=0.0.0.0` | P4 从“零散 helper”推进到“多数核心写入点收口”;剩余 map tiles、pack repository、旧 env 常量、`safe_write_under_root()` fsync 注释/实现不一致 | | P5 capability truth model | 新增 `modules/core_sys/include/platform/ui/capability_status.h`,Linux 侧 `platform/linux/capability_status.h` 变为 re-export;LoRa/Walkie/SSTV 实现层返回 `Simulated` | 概念已经进入合约层,但 public runtime headers 尚未声明 `capability_status()`,contract README 未列入 `capability_status.h`,UI 也还没消费;因此只能算“合约种子落地”,不能算能力呈现完成 | | P6 demo world/facade 拆分 | `runtime_mode` 继续只 gate demo seed;`MinimalLinuxAppFacade` 仍总是组合 loopback mesh、dummy crypto、loopback pairing | demo/local 的数据种子边界更清楚,但真实设备 composition 仍不干净;另有 Windows simulator 会覆盖用户预设 `TRAIL_MATE_RUNTIME_MODE` 的小坑 | | P7 M5 SDK 主路径 | `apps/linux_rpi/main/src/main.cpp` 已具备 ST7789 framebuffer 探测、LVGL fbdev、LVGL evdev pointer/keypad、启动日志和 bring-up UI;shared shell 接入代码仍在注释中 | SDK path 已不再是空壳,但仍是 bring-up UI,不是 Trail Mate shared shell;L5 仍未完成 | | P8 第一个验证 slice | 本轮未新增有效 build/ctest 结果;已有本地 build 受 Windows clang/MSVC STL 版本拦截 | 继续保持“代码阅读评估 + 边界检查可验证”,最终结论仍要等 Linux/WSL CI 或真机验收 | 一句话:最新推进把 P4/P5/P7 的结构成熟度又抬了一档,但 Linux 线仍停在“结构越来越像最终形态、验证还没闭环”的阶段。下一步最优先不是继续加功能,而是把这些结构跑通:Linux/WSL build、capability public API、SDK shared shell 接入、真机输入采样。 ### 0.6 2026-05-08 再次回归结论 本轮相对 `0.5` 又推进了三个上一轮点名的缺口:simulator runtime mode 覆盖语义、LoRa/SSTV/Walkie capability public API、map tile runtime path。结论如下: | 指南项 | 最新观察 | 当前判断 | | --- | --- | --- | | P0-E runtime mode 覆盖语义 | `apps/linux_sim/src/targets/simulator_main.cpp` 现在只在 `TRAIL_MATE_RUNTIME_MODE` 未设置时写入 `demo`;Windows `_putenv_s()` 也被放到同一个 guard 后面 | 上轮指出的 Windows 覆盖用户预设问题已修;剩余是用 Windows/WSL 启动 smoke 验证 env override | | P4 runtime path/env | `platform/linux/common/src/ui/widgets/map/map_tiles.cpp` 已改为 `resolve_paths().sd_root`;hostlink/team/SSTV/map tiles 都不再复制完整 storage root fallback | P4 继续变好;仍剩 `pack_repository_runtime.cpp` 默认从 `__FILE__` 推 repo root,map tiles 里旧 `kSdRootEnv`/`kSettingsRootEnv` 常量已无用途,`runtime_paths.h` 头文件仍写 fsync 但实现已改成 no-fsync temp+rename | | P5 capability truth model | `lora_runtime.h`、`sstv_runtime.h`、`walkie_runtime.h` 已 include `platform/ui/capability_status.h` 并公开声明 `CapabilityStatus capability_status()` | P5 从“合约种子落地”推进到“部分 runtime public API 落地”;剩余是更新 `modules/core_sys/include/platform/ui/README.md` contract inventory,并让 UI 消费该状态 | | P7 M5 SDK 主路径 | `apps/linux_rpi/main/src/main.cpp` 仍是 bring-up UI,shared shell 接入仍停在注释 | 本轮未推进;L5 仍未完成 | | P8 验证 slice | 本轮重新跑了 boundary check,通过;仍未完成 Linux/WSL build、ctest 或真机验证 | 边界干净,但 build/run gate 仍是下一步硬门槛 | 一句话:这轮把 `0.5` 里的几项“结构缺口”又补了一层,尤其是 P0-E 和 P5 public API。当前最值得立刻做的不是继续写更多 runtime,而是清理两个残留文档/API 小口子,然后跑 Linux/WSL 构建和至少一个 capability UI 消费闭环。 ## 1. 先做区分 ### 1.1 当前最容易混在一起的概念 Linux 适配不能只理解成“让代码能在 Linux 编译”。当前仓库里至少有这些必须分开的对象: | 概念 | 它是什么 | 它不是什么 | | --- | --- | --- | | Linux 目标线 | Trail Mate 的一等平台族 | 不是 ESP/Arduino 的另一个 board profile | | `apps/linux_sim` | 桌面模拟器和开发工具壳 | 不是真机运行时,不应承接 Pi OS 硬件细节 | | `apps/linux_rpi` | Pi OS / Cardputer Zero 真机 app shell | 不应吸收模拟器几何、桌面输入或 demo app 结构 | | `platform/linux/common` | simulator 与 rpi 都可共享的 Linux-safe 实现层 | 不应放 SDL 外壳、fbdev 设备假设或业务规则 | | `platform/linux/rpi` | Pi OS / framebuffer / 设备适配层 | 不应放共享 UI 页面逻辑 | | `M5Stack_Linux_Libs` | 真实设备 SDK 基线 | 不是 Trail Mate 架构边界的来源 | | `M5CardputerZero-UserDemo` | 板级线索参考 | 不是项目模板 | | 平台合约 | `modules/core_sys/include/platform/ui/*` 中的协作面 | 不是某个平台的实现细节 | | 平台实现 | `platform/esp/*`、`platform/linux/*` 中的具体实现 | 不拥有 domain/usecase/UI 结构 | | 能力支持 | 真实或可解释的运行时能力 | 不是“菜单上有入口”或“有一个模拟桩” | ### 1.2 当前 specification 基线 后续 Linux 适配应遵守以下基线: - `modules/core_*` 是核心业务、协议、策略、用例、状态和纯工具层。 - `modules/ui_shared` 是共享 LVGL 表现层,可依赖核心模块和平台合约,不可依赖平台实现。 - `modules/core_sys/include/platform/ui/*` 是共享 UI 与平台实现之间的合约层。 - `platform/linux/common` 实现 Linux 共享合约,只放 simulator 与 device 都成立的东西。 - `platform/linux/rpi` 实现 Pi OS / Cardputer Zero 特有适配。 - `apps/linux_sim` 和 `apps/linux_rpi` 只做 composition root、启动和目标选择。 - `M5Stack_Linux_Libs` 应位于 Trail Mate 平台边界之下,由 Linux device shell 或 `platform/linux/rpi` 消费。 ### 1.3 明确非法的切法 这些做法会让 Linux 适配变得不优雅,后续应避免: - 把 `Cardputer Zero` 当成另一个 MCU board,强行通过 `BoardBase` 抽象接入。 - 把 SDL、fbdev、evdev、`/dev/input/*`、`/dev/fb*` 写进 `ui_shared`。 - 把 `M5CardputerZero-UserDemo` 的目录结构或 app 组织方式复制进仓库。 - 把“环境变量可模拟”误认为“真机能力已支持”。 - 把 hostlink、GPS、SSTV、LoRa 的模拟数据当成产品真实能力。 - 因为 CMake 中两个 Linux app 都需要同一批源码,就长期手写两份 source list。 - 在 shared/core 里撒 `#ifdef __linux__` 或 `#ifdef _WIN32` 来逃避边界设计。 - 在 `apps/linux_rpi` 里直接堆真机驱动、UI 页面和业务逻辑。 ## 2. 当前适配程度 ### 2.1 成熟度分级 这里的等级不是测试覆盖率,而是架构和产品可用性的成熟度。 | 等级 | 含义 | 当前状态 | | --- | --- | --- | | L0 | 有目录和文档,Linux 被识别为目标线 | 已完成 | | L1 | Linux host 可以配置、编译基础目标 | 历史上成立;本轮结构改动后仍需要重新跑 build gate | | L2 | 桌面模拟器能跑共享 shell,形成快速验证闭环 | 结构成立;仍需重新证明 simulator build/test | | L3 | Linux common runtime 覆盖主要平台合约,有 smoke tests | 基本完成且本轮有 path/capability 进展;`CapabilityStatus` 已上移为合约种子并进入 LoRa/SSTV/Walkie public header,path smoke 源码已修正但未验收 | | L4 | Pi OS framebuffer/device shell 可编译并启动共享 UI | 结构推进明显,evdev source wiring 已补;SDK bring-up shell 更完整;仍未 build/run-closed | | L5 | `M5Stack_Linux_Libs` 主路径跑 Trail Mate 共享 shell | SDK bring-up 已加强,但 shared shell 尚未接入 | | L6 | 真机显示、输入、存储、GPS、音频、网络/无线能力按能力模型闭环 | 未完成 | | L7 | 可安装、可发布、可回归、可支持多 Linux 设备族 | 未完成 | 当前整体结论:L3 的方向和代码面都成立,但本轮改动后仍必须重新 build-closed;L4 已经从 display-only 走向 display+input + SDK bring-up 的结构阶段,但仍属于未验收状态;L5 还没闭环。 ### 2.2 分项评估 | 维度 | 当前程度 | 证据 | 缺口 | | --- | --- | --- | --- | | 目录结构 | 较好 | `apps/linux_sim`、`apps/linux_rpi`、`apps/linux_unoq`、`platform/linux/common`、`platform/linux/rpi` 已存在;本轮新增 `cmake/TrailMateLinuxSources.cmake` | `platform/linux/unoq` 还只有规划;Linux docs 与 app-local specs 的层级关系仍需维护 | | 架构规范 | 较好 | `docs/LINUX_ADAPTATION_GUIDE.md` 与 `apps/linux_rpi/docs/specification/*` 已形成 final-shape / checklist / 回归指南组合 | app-local specs 里个别 `DONE` 状态比代码更乐观,需要用 build/test 结果校准 | | 边界保护 | 较好 | `scripts/check_platform_ui_boundaries.py` 存在并保护 shared/core 不被 ESP 污染 | 规则还偏 include 污染,尚未覆盖 CMake/source ownership、能力语义、路径安全、demo/real composition | | 模拟器 | 较好 | `apps/linux_sim` 有 SDL3 simulator、CMake presets、tests、WSL/dev-container 脚本,并开始复用 shared CMake helper | 本轮改动后要重新证明 simulator build/test;模拟器能力和真实能力界线还需更强标注 | | Linux common runtime | 中等偏好 | settings/time/screen/device/GPS/hostlink/tracker/route/SSTV/walkie/LoRa/pack repository 等已实现;runtime path/env/capability 基础继续推进,hostlink/team/SSTV/map tiles 已开始收口到 `runtime_paths` | 很多是 soft runtime 或 synthetic runtime,不应直接宣称真机支持;pack repository 仍有 ad hoc path root;capability 尚未进入 UI | | Pi OS CMake device shell | 中等偏早 | custom framebuffer target 使用共享 shell runner,已从 `EvdevInput` drain 真机输入,rpi CMake 已编入 evdev 源文件,key map 已改为 `std::to_array` | 仍需 Linux 构建确认;Cardputer Zero 真机键位验证仍未闭环 | | M5Stack SDK path | 早期偏中 | `apps/linux_rpi/SConstruct`、`main/SConstruct`、`config_defaults.mk`、`main/src/main.cpp` 已存在;main 已具备 ST7789 framebuffer 探测、LVGL fbdev、LVGL evdev pointer/keypad 和启动日志 | SDK path 目前仍是 bring-up UI,shared shell 接入只在注释中,未复用 Trail Mate `ShellSession` / `NativeLvglHost` | | UI 共享 | 中等偏好 | `trailmate_cardputer_zero_ui_shell` 编译大量 `modules/ui_shared` 页面和资产;新增 `ShellSession`/`CanvasLvglHost`/`NativeLvglHost`,输入事件吞掉问题已修,callback 已回到 LVGL 精确类型签名 | SDK native path 尚未接入;仍需完整构建确认 LVGL typedef/前向声明兼容性 | | CMake 结构 | 明显改善 | linux_sim 与 linux_rpi 已改为 include 共享 helper,source list 不再主要散在两个 app CMake 里,rpi-only evdev source 已接入,helper 路径注释已校准 | 需要跑 sim/rpi build;target compile definitions 和 runtime mode 默认语义还需收口 | | 真机能力 | 早期 | fbdev path、LVGL evdev bring-up、NMEA serial path、hostlink TCP path 已有雏形;hostlink 默认 loopback 更安全 | Cardputer Zero 键位、battery、brightness、Wi-Fi、USB、firmware update、real radio/audio 等未闭环 | | CI | 中等偏好 | `.github/workflows/linux-simulator.yml` 覆盖 simulator build/tests;`.github/workflows/uconsole-linux.yml` 覆盖 uConsole GTK build/tests/package | Cardputer Zero Linux 不再借用这个 workflow 名义;仍不构建 M5 SDK path,不跑真机/虚拟 fbdev,不验证 Docker GUI path | | 开发体验 | 较好 | Windows PowerShell、Linux shell、WSL validate、Docker dev container 脚本齐全 | 本地环境变量和 path bridge 还在演进;不同 host 的路径/权限策略需要统一文档化 | ### 2.3 已经做得比较好的地方 1. Linux 不是外挂目录,而是进入了目标结构。 `apps/README.md`、`docs/ARCHITECTURE.md`、`platform/linux/README.md` 都表明 Linux 目标线已被纳入长期结构。 2. Simulator 与 real device 的边界已经开始分开。 `apps/linux_sim` 的 README 明确 simulator-first,`apps/linux_rpi` 的 README 明确 Pi OS device shell。`platform/linux/common` 被定义为两者共享层。 3. shared UI 已经能被 Linux shell 消费。 Linux CMake target 编译大量 `modules/ui_shared/src/ui/*` 页面、组件、资产,并通过 `SharedUiShellStartup` 进入共享 boot/menu shell。 4. 核心模块平台污染已经显著改善。 当前边界检查通过,`modules/ui_shared/library.json` 已不再带 ESP Arduino include root。过去规范中提到的某些违规项已经被修掉。 5. Linux common runtime 已经不是空壳。 `platform/linux/common/src/platform/ui/*` 已经覆盖了一批真实或可模拟的能力:文件设置、屏幕 timeout、GPS NMEA、hostlink TCP、tracker/route 文件、team UI store、pack repository、SSTV/walkie/LoRa synthetic runtime 等。 6. CMake 重复已经开始收口。 `cmake/TrailMateLinuxSources.cmake` 把 common sources、UI shell sources、include roots 和 warnings helper 提到了共享层。后续添加 shared UI 文件时,不应再让 simulator 和 rpi 各自维护一份大 source list。 7. LVGL ownership 的正确方向已经出现。 `ShellSession` 开始只拥有 app facade、startup、事件队列和 per-frame app tick;`CanvasLvglHost` 拥有 `lv_init()`、display、RGB565 buffer 和 canvas copy;`NativeLvglHost` 给 SDK-owned LVGL path 留了入口。这是接入 `M5Stack_Linux_Libs` 的正确中间形态。 8. 真机输入和路径安全都开始从“口头要求”变成代码。 `EvdevInput` 已经把 `/dev/input/event*`、by-path/by-id 键盘探测、`TRAIL_MATE_INPUT_DEVICE` override、Linux key code 到 `InputEvent` 的映射放到了 `platform/linux/rpi`。`runtime_paths` 和 `resolve_child_under_root()` 也让 route/tracker 删除开始具备 root containment;最新推进还把 hostlink、team store、SSTV 等更多写入点收口到同一套 runtime root。 9. CI 已经能挡住基础倒退。 专门的 `Linux Simulator` workflow 会跑 simulator build 和 smoke tests;`uConsole Linux` workflow 会跑 uConsole GTK build、smoke tests 和 Debian package。Cardputer Zero Linux 需要单独的 device-shell / hardware validation gate,当前不再用 workflow 名称暗示它已经完成。 10. 能力真假开始从 Linux 私有概念上移到公共合约。 `modules/core_sys/include/platform/ui/capability_status.h` 已经成为 `Unsupported` / `Simulated` / `Available` / `Degraded` / `Error` 的共享类型来源,Linux 侧 `platform/linux/capability_status.h` 退化为 re-export。LoRa/SSTV/Walkie 的 public header 已经公开 `capability_status()`,这是好方向;下一步要更新 contract inventory,并让 UI 真正消费这些状态,而不是只在实现层或文档层停留。 ### 2.4 主要未完成项 1. 真机主路径尚未闭环。 当前有两个 device path: - CMake custom framebuffer path:接了 shared shell,并已把 evdev 输入源文件接入 rpi target;但还没有通过 Linux 构建和真机操作验收。 - `M5Stack_Linux_Libs` SCons path:更接近未来真机主路径,目前已具备 fbdev 自动探测、LVGL evdev、启动日志和 bring-up UI,但还没有运行 Trail Mate shared shell。 这两个路径需要收敛:优先让 SDK path 跑 shared shell,同时保留 custom framebuffer path 作为轻量 fallback 或验证工具。 2. LVGL ownership 已经拆出雏形,但还未验收。 当前 `ShellSession`、`CanvasLvglHost`、`NativeLvglHost` 的职责划分比上一版正确很多。新的问题是: - `shell_ui_runner.h` 已前向声明 `lv_area_t`、`lv_indev_data_t`,callback 已恢复为 LVGL 精确类型签名。后续必须通过完整构建确认这些前向声明是否与 LVGL 自身 typedef 兼容;若 LVGL 使用匿名 typedef,最稳妥的做法是把 callback 改成 `.cpp` 私有自由函数或直接在 header 包含 `lvgl.h`。 - `readInputCb()` 的事件吞掉问题已经通过 `hasPendingKeyEvent()` 修掉,但需要用测试或真机输入回归确认。 - `NativeLvglHost` 只是 thin host,还没有被 `M5Stack_Linux_Libs` main loop 消费。 3. 输入已经开始适配设备,但有阻断点。 SDL 模拟器有完整键盘和鼠标映射。Pi CMake framebuffer path 现在通过 `EvdevInput` 返回 `InputEvent`,这是关键进展。上一轮的 source wiring 和目录探测问题已基本修掉。剩余必须修: - `EvdevInput` 的 `kKeyMap` 已改为 `std::to_array({...})`,手写数量和 CTAD 风险已消除;后续重点是 Linux 构建确认与真机键位采样。 - Cardputer Zero 内置键盘的 Fn/组合键/方向键需要用真机事件码采样后建立映射表,不要只依赖 PC keyboard key code。 4. 能力真假还需要制度化。 例如: - `lora::is_supported()` 当前返回 true,但实现是 synthetic RSSI。 - `walkie::is_supported()` 当前返回 true,但实现是 synthetic 音量/收发电平。 - `sstv::is_supported()` 当前返回 true,但实现是生成 PPM 模拟图。 - `wifi::is_supported()`、`usb_support::is_supported()`、`firmware_update::is_supported()` 返回 false,这类比较诚实。 本轮新增 `CapabilityState`/`CapabilityStatus` 是正确方向,而且最新代码已经把它放进 `modules/core_sys/include/platform/ui/capability_status.h`,这意味着它开始成为平台合约。LoRa/SSTV/Walkie 的 public header 已经声明 `capability_status()`。剩余问题是:`modules/core_sys/include/platform/ui/README.md` 的 contract inventory 还没有列出 `capability_status.h`;共享 UI 还没有用该状态替代“只看 `is_supported()`”。后续需要制度化区分 `Unsupported`、`Simulated`、`Available`、`Degraded`、`Error`,否则 UI 和用户预期会失真。 5. 构建描述重复已经缓解,但 build gate 尚未通过。 `cmake/TrailMateLinuxSources.cmake` 已经解决了最大块的 source list 重复。剩余问题是: - rpi-only 源文件已经有显式接入点,后续可考虑抽成 rpi adapter target,但不必为了形式立即重构。 - simulator main 已显式设置默认 `TRAIL_MATE_RUNTIME_MODE=demo`,这比让 common target define 决定 simulator 默认行为更清楚。最新代码已把默认写入放到 `std::getenv("TRAIL_MATE_RUNTIME_MODE")` guard 后面,Windows 和 POSIX 语义已经对齐;后续还要用启动 smoke 验证用户预设不会被覆盖,并确认 device shell 默认 `local`,让真实设备 composition 不再默认使用 dummy/loopback。 6. 文件路径安全已经改善,但还没有覆盖所有写入点。 `route_storage::remove_route()`、`tracker::remove_track()` 已经开始用 `resolve_child_under_root()`。最新推进又把 hostlink、team store、SSTV output、map tiles 的默认 root 收到了 `runtime_paths`。下一步要把同一套 path helper 扩展到 pack repository、settings temp file 等路径写入点,并清理 map/hostlink/team/SSTV 中已经无实际用途的旧 env 常量。`safe_write_under_root()` 的 `.cpp` 注释已经改成 no-fsync temp+rename,但 header 仍写 fsync;要么补真实 fsync / `FlushFileBuffers`,要么把 header 注释改成准确描述。 7. demo/loopback 逻辑只完成了抽取起点。 `linux_demo_world.*` 已经出现,demo peer seeding 已经通过 `runtime_mode` gate;但 `MinimalLinuxAppFacade` 里仍保留旧 `LinuxLoopbackMeshAdapter`、dummy crypto、loopback pairing service 等真实设备不应默认启用的逻辑。`runtime_mode` 还没有真正决定整个 facade composition。 8. 真机安全和网络默认值需要持续保持谨慎。 `hostlink` 当前已经默认 bind 到 `127.0.0.1`,这比上一版更适合真机安全默认值。需要对外监听时应显式设置 `TRAIL_MATE_HOSTLINK_BIND=0.0.0.0`,并在 UI 或文档里说明这会把端口暴露到设备所在网络。 ## 3. 目标结构规划 ### 3.1 最终目录职责 推荐目标结构保持如下: ```text trail-mate/ apps/ linux_sim/ # 桌面模拟器、WSL、dev-container、host tooling linux_rpi/ # Cardputer Zero / Pi OS 真机 app shell linux_unoq/ # 未来 UNO Q Linux shell modules/ core_sys/ # 系统合约、clock、portable utilities core_chat/ # chat domain/usecase/protocol-neutral infra core_gps/ # GPS domain/filter/policy core_team/ # team domain/protocol/usecase core_hostlink/ # hostlink protocol/session ui_shared/ # LVGL shared UI and presentation platform/ linux/ common/ # Linux-safe implementations shared by sim and rpi rpi/ # Pi OS/Cardputer Zero specific adapters unoq/ # UNO Q specific adapters when introduced esp/ ... shared/ ... ``` ### 3.2 依赖方向 合法方向: ```text apps/linux_* -> platform/linux/* apps/linux_* -> modules/ui_shared apps/linux_* -> modules/core_* modules/ui_shared -> modules/core_* modules/ui_shared -> platform contracts in modules/core_sys/include/platform/ui/* platform/linux/* -> platform contracts platform/linux/* -> OS / SDK / drivers modules/core_* -> standard C/C++ and portable module dependencies only ``` 非法方向: ```text modules/core_* -> platform/linux/* modules/core_* -> platform/esp/* modules/ui_shared -> platform/linux/* modules/ui_shared -> platform/esp/* platform/linux/common -> apps/linux_sim/* platform/linux/common -> apps/linux_rpi/* apps/linux_rpi -> apps/linux_sim/* ``` ### 3.3 建议新增的结构 后续可以逐步增加这些结构,不要求一次性完成: ```text cmake/ TrailMateLinuxSources.cmake TrailMateModuleTargets.cmake modules/core_sys/include/platform/ui/ capability_status.h platform/linux/common/include/platform/linux/ runtime_paths.h capability_status.h # re-export only; shared contract lives in modules/core_sys env_config.h platform/linux/common/src/platform/linux/ runtime_paths.cpp env_config.cpp safe_file_ops.cpp platform/linux/rpi/src/platform/device/ evdev_input.cpp evdev_input.h m5stack_lvgl_shell_host.cpp m5stack_lvgl_shell_host.h platform/linux/common/include/ui/ shell_session.h platform/linux/common/src/ui/ shell_session.cpp canvas_lvgl_host.cpp native_lvgl_host.cpp ``` 核心意图: - CMake source ownership 要集中。 - path/env/capability 不要在十几个 runtime 文件里重复。 - LVGL session 要和 LVGL display owner 分离。 - 真实设备输入要成为 `platform/linux/rpi` 的 adapter,不要写进 shared UI。 ## 4. 接下来要做什么 先看本轮回归后的优先级,不要直接沿用上一版 backlog 的顺序: | 优先级 | 事项 | 为什么现在要先做 | 完成标准 | | --- | --- | --- | --- | | P0-A | 完成构建闭环 | 主要源码红灯已修,下一步需要用真实构建确认 common/ui/rpi/test 都成立 | sim common、ui shell、path safety smoke、rpi framebuffer target 都能编译 | | P0-B | 回归 Shell input callback | 事件吞掉问题已修成 peek 语义,但需要验证 press/release 是否都被 LVGL 收到 | 单个 `InputEvent` enqueue 后 LVGL 能看到 press 和 release | | P0-C | 验证 evdev path 正常参与 rpi 构建 | `evdev_input.cpp` 已进入 rpi target,但还没有 Linux/CI build 结果 | rpi framebuffer target 编译通过;无输入设备时只降级不崩溃 | | P0-D | 校准 docs/spec 的 DONE 状态 | app-local specs 里有些 DONE 是目标态,不是已验收事实 | 文档中的 DONE 只代表经过 build/test/运行验证的事实 | | P0-E | 验证 simulator runtime mode 覆盖语义 | 代码已改为只在 env 未设置时填入 `demo`,Windows `_putenv_s()` 也已进入 guard | Windows 与 POSIX 都通过启动 smoke 证明用户预设不会被覆盖 | | P1 | 继续 CMake helper 收口 | helper 已存在,下一步是让它稳定、可维护、可 CI 检查 | 添加 shared source 只改一处;sim/rpi CI 都绿 | | P2 | 完成 LVGL host/session 分层 | 这是接 SDK path 的前提 | simulator/custom fbdev 使用 `CanvasLvglHost`,SDK path 使用 `NativeLvglHost` | | P3 | 真机输入采样与映射 | evdev adapter 只解决了读事件,还没证明 Cardputer Zero 内置键盘语义 | 有事件码采样记录、映射表、真机导航验收 | | P4 | 统一 runtime path/env/capability | 基础 helper 已覆盖更多 runtime,map tiles 已收口;pack repository、旧 env 常量和部分注释仍未收口 | 所有文件 runtime 走同一套 root/path helper;各 runtime public header 能报告 capability status;synthetic 能力不再冒充真实 | | P5 | demo world 与真实 facade 分离 | 新文件已出现,但 composition 未变 | `SimulatorDemo`、`DeviceLocal`、`DeviceRealMesh` 使用明确不同的 wiring | | P6 | SDK path 接 shared shell | SDK bring-up 已加强,正好可以切换到 `NativeLvglHost` | `apps/linux_rpi/main/src/main.cpp` 不再是 bring-up UI,而是 Trail Mate shared shell | ### P0:把现状文档和 specification 校准 目标:文档不能继续描述已经修掉的债务,也不能把当前 demo 能力写成真机能力。 要做: - 保留 `apps/linux_rpi/docs/specification/*` 作为历史和 target-specific specification。 - 以本文作为顶层 Linux 适配总指南。 - 更新旧文档中已经过期的“known current violations”,例如 `ui_shared/library.json` 的 ESP include 依赖已经不再成立。 - 在 README 中明确本文与 app-local specs 的层级关系。 验收: - 新同事读 `docs/LINUX_ADAPTATION_GUIDE.md` 能判断文件应该放哪里。 - 旧文档不再误导工程师去修已不存在的问题。 ### P1:消除 Linux CMake source list 重复 回归状态:已开始落地。`cmake/TrailMateLinuxSources.cmake` 已经承担 shared source list、include roots 和 target helper,rpi-only `evdev_input.cpp` 也已显式接入 device target,helper 的 repo root 注释已经校准。下一步不是重新设计,而是确认 target define 语义,并让 simulator/rpi 两条构建都通过。 目标:让 `linux_sim` 与 `linux_rpi` 共享同一套 CMake source/target 定义。 建议做法: 1. 新建 `cmake/TrailMateLinuxSources.cmake`。 2. 抽出这些变量: ```cmake set(TRAIL_MATE_LINUX_COMMON_SOURCES "${TRAIL_MATE_LINUX_COMMON_ROOT}/app/linux_app_facade.cpp" "${TRAIL_MATE_LINUX_COMMON_ROOT}/platform/ui/settings_store.cpp" ... ) set(TRAIL_MATE_UI_SHARED_SOURCES "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/app_runtime.cpp" ... ) ``` 3. 提供 helper: ```cmake function(trailmate_add_linux_common target_name) add_library(${target_name} STATIC ${TRAIL_MATE_LINUX_COMMON_SOURCES}) target_include_directories(${target_name} PUBLIC ...) target_compile_features(${target_name} PUBLIC cxx_std_20) endfunction() function(trailmate_add_linux_ui_shell target_name common_target) add_library(${target_name} STATIC ${TRAIL_MATE_UI_SHARED_SOURCES}) target_link_libraries(${target_name} PUBLIC ${common_target} lvgl) endfunction() ``` 4. `apps/linux_sim/CMakeLists.txt` 只保留 simulator 特有 SDL target。 5. `apps/linux_rpi/CMakeLists.txt` 只保留 device 特有 framebuffer target。 注意: - 先抽 source list,不急着把所有 `modules/core_*` 都变成独立 CMake target。 - 抽完后确保 CI、WSL validate、Windows VS preset 仍能工作。 验收: - 添加一个 shared UI 源文件时,只需要改一处 CMake source list。 - simulator 与 rpi CMake target 使用同一批 common/ui sources。 ### P2:拆分 LVGL shell session 与 display owner 回归状态:已开始落地。`ShellSession`、`CanvasLvglHost`、`NativeLvglHost` 的形状是对的;input dequeue 语义已修成 peek,LVGL callback 也已恢复为精确 typed signature。当前还需要完整构建确认 LVGL typedef/前向声明兼容性,并接入 SDK main loop。 目标:同一套 Trail Mate shared shell 可以跑在 SDL/canvas,也可以跑在 `M5Stack_Linux_Libs` 已创建的 LVGL display 上。 当前问题: - `ShellUiRunner` 负责 `lv_init()`、display 创建、buffer 创建、input 创建、app facade、startup、tick、canvas copy。 - 这对模拟器方便,但对 SDK path 不优雅,因为 SDK 已经拥有 fbdev/evdev/LVGL backend。 建议拆法: ```text ShellSession - 绑定 MinimalLinuxAppFacade 或未来 LinuxAppFacade - setTeamUiEventDispatcher - SharedUiShellStartup begin/tick - app facade update/tick/dispatch - lv_timer_handler 前后的共享逻辑 - 不创建 display,不拥有 lv_init/lv_deinit CanvasLvglHost - 给 simulator/custom fbdev 用 - 拥有 lv_init/lv_deinit - 创建 LVGL display 和 RGB565 buffer - flush 后复制到 Canvas NativeLvglHost - 给 M5Stack SDK path 用 - 不拥有 display - 使用 SDK 已创建的 LVGL display/input - 只驱动 ShellSession tick ``` 实现细节: - `lv_init()` 每个进程只能有清晰 owner。不能 simulator、SDK、shell session 各自调用。 - `lv_deinit()` 只能由 owner 调。 - `ShellSession` 析构时只解绑 facade 和 dispatcher,不删除它不拥有的 display/input。 - `ShellSession::tick()` 不应该 sleep。sleep 由 host runner 决定。 - `CanvasLvglHost` 继续用 16 ms frame pacing。 - `NativeLvglHost` 在 SDK main loop 里按 SDK 推荐 tick cadence 调用。 验收: - simulator 仍可跑。 - CMake custom framebuffer shell 仍可跑。 - SDK `main/src/main.cpp` 能从 bring-up UI 切到 Trail Mate shared shell。 ### P3:实现真机输入适配 回归状态:已开始落地。`EvdevInput` 已经出现并被 `LinuxFramebufferPlatform::drainInput()` 调用,rpi CMake source wiring、by-path/by-id 目录探测异常、key map 初始化风险都已修。当前必须完成 Linux 构建确认和真机键位采样。 目标:Cardputer Zero 在 Pi OS 上能通过内置键盘/按钮操作 shared shell。 当前状态: - SDL simulator 输入完整。 - CMake framebuffer device shell 已开始接 `EvdevInput`,但还未通过编译和真机按键验收。 - SDK bring-up path 有 `LV_LINUX_KEYBOARD_DEVICE` 和 evdev hint,但尚未进入 Trail Mate 输入语义。 建议路线: 1. 短期:在 SDK path 使用 LVGL evdev keypad。 适合快速真机 bring-up。`apps/linux_rpi/main/src/main.cpp` 已经具备类似结构: ```cpp lv_indev_t* keyboard = lv_evdev_create(LV_INDEV_TYPE_KEYPAD, keyboard_device); lv_indev_set_display(keyboard, display); ``` 需要补: - 默认设备发现不要只写死某个 `/dev/input/by-path/...`。 - 支持 `LV_LINUX_KEYBOARD_DEVICE` 显式覆盖。 - 记录检测日志,失败时不要静默。 2. 中期:在 `platform/linux/rpi` 做 evdev -> `InputEvent` adapter。 适合 custom framebuffer path 和未来 UNOQ path: ```text platform/linux/rpi/src/platform/device/evdev_input.{h,cpp} ``` 职责: - nonblocking open evdev file。 - 读取 `struct input_event`。 - 处理 `EV_KEY` press/release/repeat。 - 映射 Linux key code 到 `trailmate::cardputer_zero::app::InputEvent`。 - 允许 env 覆盖:`TRAIL_MATE_INPUT_DEVICE`。 - 支持多个候选路径:`/dev/input/by-path/*-event-kbd`、`/dev/input/event*`。 - 不把 evdev 头文件暴露到 `platform/linux/common`。 映射建议: | Linux key | Trail Mate key | | --- | --- | | `KEY_ESC` | `Power` 或 back,取决于设备语义 | | `KEY_ENTER` | `Enter` | | `KEY_BACKSPACE` | `Backspace` | | `KEY_TAB` | `Tab` | | `KEY_HOME` | `Home` | | `KEY_END` | `Next` | | `KEY_LEFT/RIGHT/UP/DOWN` | directional keys | | printable ASCII | `Character` | | `KEY_LEFTSHIFT/RIGHTSHIFT` | `Shift` | | `KEY_LEFTCTRL/RIGHTCTRL` | `Ctrl` | | `KEY_LEFTALT/RIGHTALT` | `Alt` | 实现细节: - 输入 adapter 不应该直接调用 LVGL。 - `SurfacePresenter::drainInput()` 可以短期继续承载输入队列,但长期可拆出 `InputSource`。 - repeat 策略要明确:导航键可以 repeat,文本输入可以按系统 repeat,modifier 不 repeat。 - 字符输入必须考虑 shift。短期可用简单 US keyboard map;长期如需中文/IME,应走 `ui_shared` IME。 验收: - 真机按键能进入菜单、打开页面、返回、输入文本。 - `TRAIL_MATE_INPUT_DEVICE=/dev/input/eventX` 可覆盖设备。 - 没有输入设备时,device shell 明确日志提示并继续显示 UI。 ### P4:集中 Linux runtime path/env/capability 回归状态:部分落地且本轮继续推进。`runtime_paths.*`、`env_config.*` 已出现,route/tracker 的删除路径开始做 root containment;settings、hostlink、team store、SSTV、map tiles 也已经开始通过 `runtime_paths` 取默认 root。下一步是把剩余文件写入点和 env parsing 收到同一套 helper,并把 path safety smoke 纳入稳定测试。 目标:避免每个 runtime 文件重复实现 `TRAIL_MATE_SD_ROOT`、`TRAIL_MATE_SETTINGS_ROOT`、`HOME`、`APPDATA` fallback。 当前仍需继续收口的重复点: - `map_tiles.cpp` 已改用 `runtime_paths`,但旧的 `kSdRootEnv` / `kSettingsRootEnv` 常量还残留,容易造成误读或编译告警 - `pack_repository_runtime.cpp` 仍默认从 `__FILE__` 推 repo root,没有进入 runtime root 模型 - `settings_store.cpp` 已用 `settings_file()`,但 temp file 写入还没有复用 `safe_write_under_root()` - `route_storage.cpp` 和 `tracker_runtime.cpp` 已部分改用 path helper,但还需统一写入和 list 语义 - `device_runtime.cpp`、`hostlink_runtime.cpp`、`team_ui_store_runtime.cpp`、`sstv_runtime.cpp` 中仍有一些旧 env 常量或注释需要清理,避免后来者误判真实配置来源 - `runtime_paths.h` 仍写着 `safe_write_under_root()` 会 fsync,但 `.cpp` 已明确说当前只是 close + atomic rename 已有基础形态,后续保持这个方向扩展: ```cpp namespace platform::linux_runtime { struct RuntimePaths { std::filesystem::path settings_root; std::filesystem::path sd_root; std::filesystem::path cache_root; std::filesystem::path state_root; }; RuntimePaths resolve_paths(); std::filesystem::path settings_file(const char* ns); std::filesystem::path sd_child(std::string_view relative); bool resolve_child_under_root(const std::filesystem::path& root, std::string_view relative, std::filesystem::path& out); } ``` 路径策略: - `TRAIL_MATE_SETTINGS_ROOT` 显式指定 settings/state 根目录。 - `TRAIL_MATE_SD_ROOT` 显式指定模拟 SD 卡根目录。 - Linux host 默认可使用 `$HOME/.trailmate_cardputer_zero`,后续可演进到 XDG。 - Windows simulator 默认可继续使用 `%APPDATA%/TrailMateCardputerZero`。 - 真机 system service 模式后续应支持 `/var/lib/trail-mate` 或用户配置目录,但不要现在硬编码。 安全规则: - UI 删除 route/track 时只接受文件名或相对 ID,不接受任意 absolute path。 - 平台层必须做 root containment 检查。 - 拒绝 `..`、absolute path、空名字、路径分隔符混入 ID。 - 删除前 resolve canonical/weakly_canonical,确认结果仍在 root 下。 验收: - 所有 Linux runtime 使用同一套 path helper。 - route/tracker/team/sstv/hostlink/map/pack repository 不再各自复制 storage root 逻辑。 - 路径安全有单元测试。 ### P5:建立能力真实性模型 回归状态:概念已进入公共合约种子,并且 LoRa/Walkie/SSTV 已经公开 public API。`CapabilityState`/`CapabilityStatus` 已上移到 `modules/core_sys/include/platform/ui/capability_status.h`,Linux 侧 helper 只做 re-export,`lora_runtime.h`、`walkie_runtime.h`、`sstv_runtime.h` 已声明 `CapabilityStatus capability_status()`,实现层开始返回 `Simulated`。下一步是让 contract inventory 和 UI 呈现跟上。 目标:UI 与用户文档能区分“不支持”“模拟支持”“真实支持”“降级支持”。 建议新增统一状态: ```cpp enum class CapabilityState { Unsupported, Simulated, Available, Degraded, Error, }; struct CapabilityStatus { CapabilityState state; const char* message; }; ``` 短期不需要一次性改完所有合约,但不能长期停在“API 存在但无人消费”的状态。建议先做三件小事: - 在 `modules/core_sys/include/platform/ui/README.md` 的 contract inventory 中列出 `capability_status.h`。 - 保持 `lora_runtime.h`、`walkie_runtime.h`、`sstv_runtime.h` 的声明与 Linux 实现一致,并在后续新增 runtime 时沿用同一模式。 - 在至少一个共享 UI status/footer 或 diagnostics 页面中展示 `Simulated`,验证 UI 不再把 synthetic runtime 当成真实支持。 最终最好让关键 runtime 暴露状态: - Wi-Fi - GPS - LoRa - Walkie - SSTV - Hostlink - USB mass storage - Firmware update - Battery/power - Display/input 当前建议分类: | 功能 | 当前代码状态 | 建议能力状态 | | --- | --- | --- | | Settings store | file-backed | Available | | Time offset | settings + system clock | Available | | Screen timeout | soft idle state | Degraded | | Device memory | `/proc/meminfo` on Linux | Available/Degraded | | Battery | env only | Simulated | | GPS default data | env/default | Simulated | | GPS NMEA file/serial | parser + Linux serial | Degraded to Available, 取决于设备 | | Hostlink TCP | local TCP server,默认 loopback | Available, 对外监听必须显式配置 | | Route/tracker storage | file-backed | Available with safety fix | | Team store | memory + GPX append | Degraded | | LoRa RSSI | synthetic | Simulated | | Walkie | synthetic | Simulated | | SSTV | generated frame | Simulated | | Wi-Fi | unsupported | Unsupported | | USB mass storage | unsupported | Unsupported | | Firmware update | unsupported | Unsupported | | Orientation | empty | Unsupported | 验收: - UI 不会把 synthetic LoRa/walkie/SSTV 展示成真实硬件能力。 - 文档和 status message 能解释为什么不可用或仅模拟。 ### P6:拆出真实 Linux app facade 与 demo world 回归状态:完成了第二步。`linux_demo_world.*` 已新增,demo seeding 已由 `runtime_mode` gate;但 facade 仍然直接拥有 loopback mesh、dummy crypto、loopback pairing。下一步要让 runtime mode 决定完整 composition,而不是让真实设备默认继承 simulator demo world 的通信与加密假实现。 目标:把“为模拟器好用的假世界”和“真实设备 facade”分开。 当前 `MinimalLinuxAppFacade` 同时做了: - app config persistence - service composition - demo peers seed - loopback mesh adapter - team pairing loopback - dummy team crypto - event bus bridge - fallback team UI updates 建议拆分: ```text platform/linux/common/src/app/ linux_app_facade.cpp linux_app_facade.h linux_demo_world.cpp linux_demo_world.h linux_loopback_mesh_adapter.cpp linux_loopback_mesh_adapter.h linux_team_runtime.cpp linux_team_runtime.h linux_app_composition.cpp linux_app_composition.h ``` 然后区分 composition mode: ```cpp enum class LinuxRuntimeMode { SimulatorDemo, DeviceLocal, DeviceRealMesh, }; ``` 关键要求: - dummy crypto 只能在 `SimulatorDemo` 使用。 - real device path 禁止使用 XOR crypto 伪实现。 - demo peer seed 只能由 simulator 或显式 demo mode 启用。 - app facade 不应该知道“显示器是 SDL 还是 fbdev”。 验收: - 启动 simulator 仍有 demo 数据。 - 启动 device shell 默认不注入假 peer,除非显式 `TRAIL_MATE_DEMO_WORLD=1`。 - dummy crypto 不会进入真机 release build。 ### P7:让 `M5Stack_Linux_Libs` 成为真机主路径 目标:真实 Cardputer Zero 设备路径优先使用 SDK 的 LVGL/fbdev/evdev/device plumbing。 步骤: 1. 保留 `apps/linux_rpi/main/src/main.cpp` 里的 display/input detect 逻辑,但把 bring-up UI 替换成 shared shell session。 2. 在 SDK main 中: ```cpp lv_init(); initLinuxDisplay(); trailmate::cardputer_zero::linux_ui::ShellSession shell; shell.begin(); while (true) { shell.tick(); lv_timer_handler(); usleep(1000); } ``` 实际代码以拆分后的 `ShellSession` API 为准。 3. `config_defaults.mk` 继续启用: ```text CONFIG_V9_5_LV_USE_LINUX_FBDEV=y CONFIG_V9_5_LV_USE_EVDEV=y ``` 4. 自动检测 framebuffer: - 优先 `LV_LINUX_FBDEV_DEVICE` - 其次 `/proc/fb` 中的 `fb_st7789v` - 最后 `/dev/fb0` 5. 自动检测 keyboard: - 优先 `LV_LINUX_KEYBOARD_DEVICE` - 其次 by-path/by-id 候选 - 最后明确报错但不中断显示 验收: - `bash apps/linux_rpi/scripts/build-sdk-device.sh` 能构建 shared shell。 - 真机能显示 boot/menu shell。 - 真机键盘能导航。 - 不再用 SDK bring-up placeholder 文案作为默认 UI。 ### P8:选择第一个真实 feature verification slice 建议第一个真实页面选择 `Settings`,原因: - 它依赖 settings/time/screen/device 这些基础合约。 - 它不需要真实 radio、GPS、audio、network。 - 它能验证 shared UI、平台合约、持久化、输入、能力状态。 - 它能暴露 Linux common runtime 的真实质量。 Settings slice 应验证: - timezone offset 保存/读取。 - screen timeout 保存/读取。 - notification volume 保存/读取。 - battery/GPS/Wi-Fi/USB/FOTA 能力状态正确显示。 - 重启 simulator/device 后设置仍存在。 - 无设备能力时 UI 不出现不可执行动作。 验收: - simulator 通过 Settings 页面进行真实设置。 - rpi SDK path 通过 Settings 页面进行真实设置。 - smoke test 或 integration test 验证 settings persistence。 ## 5. 优雅适配 Linux 的原则 ### 5.1 平台合约先于平台实现 如果一个功能需要被 `ui_shared` 使用,先确认它是否属于已有 `platform::ui::*` 合约。 - 已有合约足够:只加 Linux 实现。 - 合约缺字段:先扩展合约,再同时审视 ESP/Linux 实现。 - 没有合约:先判断这是共享能力、平台能力、还是 app shell 私有能力。 不要从 Linux 实现反推共享 API。实现方便不等于抽象正确。 ### 5.2 app shell 保持薄 `apps/linux_sim` 和 `apps/linux_rpi` 应该主要做: - main/entrypoint - build configuration - target-specific composition - process lifecycle - CLI/env parsing - selecting adapters 不要在 app shell 里长期放: - business state - page layout - protocol handling - storage format - radio/GPS/audio logic ### 5.3 common 只放真正共享的 Linux-safe 代码 进入 `platform/linux/common` 的标准: - simulator 和 rpi 都需要。 - 不依赖 SDL。 - 不依赖 fbdev/evdev/ioctl。 - 不依赖具体 device path。 - 不含真机特定假设。 - 不拥有 UI 页面结构。 如果只是 desktop simulator 需要,放 `apps/linux_sim`。 如果只有 Pi OS/Cardputer Zero 需要,放 `platform/linux/rpi` 或 `apps/linux_rpi`。 ### 5.4 真实、模拟、unsupported 必须诚实 Linux 早期开发需要大量模拟能力,这是合理的。但必须诚实标注。 推荐规则: - synthetic runtime 可用于 simulator。 - synthetic runtime 可用于真机 debug,但必须显式 opt-in。 - 真机默认 UI 不应把 synthetic runtime 展示为硬件能力。 - `is_supported()` 不要表示“有代码路径”,而要表示“此目标下用户可合理使用”。 ### 5.5 不用 `#ifdef` 替代边界 允许的 `#if defined(...)`: - 在平台实现 `.cpp` 内隔离 OS API,例如 hostlink socket、GPS serial、gmtime。 - 在 build system 中选择 target。 - 在少量 cross-host simulator 代码里处理 Windows/Linux 差异。 不推荐的 `#if defined(...)`: - 在 `modules/core_*` 里判断 Linux/ESP。 - 在 `modules/ui_shared` 页面里判断 Linux/ESP。 - 在业务逻辑里判断设备型号。 ### 5.6 SDK 是设备依赖,不是架构来源 `M5Stack_Linux_Libs` 应该帮我们少写 fbdev、evdev、LVGL glue。 它不应该决定: - Trail Mate 的模块边界。 - app/page 结构。 - core/usecase 的接口。 - storage/schema。 - Linux common 的职责。 ### 5.7 能用测试守住的边界就写测试 文档是必要的,但不够。每个容易退化的点都应尽量有自动检查: - include 污染:已有 boundary check。 - path 安全:新增 unit test。 - capability truth:新增 status test。 - CMake source ownership:通过 shared cmake helper 降低人工错误。 - simulator/device 构建:CI 继续跑。 - SDK path:至少提供可选 CI 或 manual verification script。 ## 6. 新功能开发流程 任何新的 Linux 适配功能都按这个流程走。 ### Step 1:先分类 先回答: - 这是 domain/usecase/protocol 吗?是则属于 `modules/core_*`。 - 这是 LVGL 共享页面/组件吗?是则属于 `modules/ui_shared`。 - 这是平台合约吗?是则属于 `modules/core_sys/include/platform/ui/*`。 - 这是 Linux 实现吗?是则属于 `platform/linux/common` 或 `platform/linux/rpi`。 - 这是进程启动/目标选择/build wiring 吗?是则属于 `apps/linux_*`。 ### Step 2:确认合约 检查 `modules/core_sys/include/platform/ui/*`。 如果已有合约: - 不新增平行 util。 - 不在 UI 里直接访问 Linux 文件、socket、env。 如果没有合约: - 先设计最小合约。 - 合约只表达共享语义,不表达 Linux 细节。 示例: ```cpp namespace platform::ui::wifi { bool is_supported(); bool load_config(Config& out); bool save_config(const Config& config); Status status(); } ``` 合约里不应该出现: ```cpp std::filesystem::path; int fd; struct input_event; sockaddr_in; lv_indev_t*; ``` ### Step 3:实现 Linux common 或 rpi adapter 放置规则: | 情况 | 位置 | | --- | --- | | file-backed settings | `platform/linux/common/src/platform/ui/settings_store.cpp` | | generic host TCP transport | `platform/linux/common/src/platform/ui/hostlink_runtime.cpp` | | generic NMEA parser | `platform/linux/common/src/platform/ui/gps_runtime.cpp` 或拆出 helper | | `/dev/input/event*` | `platform/linux/rpi/src/platform/device` | | `/dev/fb*` mmap/ioctl | `platform/linux/rpi/src/platform/device` | | SDL geometry/input | `apps/linux_sim/src/platform/simulator` | | M5Stack SDK glue | `apps/linux_rpi/main` 或 `platform/linux/rpi` | ### Step 4:UI 通过合约消费 `modules/ui_shared` 只能: - 调 `platform::ui::` 合约。 - 根据 status/capability 渲染 enabled/disabled/unsupported state。 - 触发合约动作。 不能: - 读 Linux env。 - 打开文件。 - 访问 `/dev/*`。 - 引入 `platform/linux/*` 头。 ### Step 5:添加测试 最少测试层级: - contract smoke:是否能调用、是否返回合理默认状态。 - persistence test:设置是否能保存/读取。 - path safety test:非法路径是否被拒绝。 - simulated data test:GPS NMEA、hostlink、tracker、team 等。 - UI smoke:如果功能影响 shell,确保 simulator boot/menu/page 不崩。 Linux tests 当前可放: ```text apps/linux_sim/tests/ ``` 随着模块化增强,可逐步迁移到: ```text tests/linux/ tests/modules/ ``` ### Step 6:更新构建 短期: - 改 shared CMake source helper。 - 确保 `apps/linux_sim` 和 `apps/linux_rpi` 都用同一处 source list。 中期: - 把 `modules/core_*` 变成 CMake targets。 - `ui_shared` 也成为 target。 - app shell 只 link targets,不手写内部文件。 ### Step 7:更新文档 每个 Linux 适配 slice 完成后,至少更新: - 本文的当前状态或 backlog。 - 对应 feature 文档。 - app README 中的运行方式。 - 如果能力状态变化,更新 capability table。 ## 7. 关键实现细节指南 ### 7.1 CMake 当前问题是两个 Linux app 都手写大 source list。优雅做法是集中 source ownership。 短期推荐: ```text cmake/TrailMateLinuxSources.cmake ``` 并在两个 app 中: ```cmake include("${PROJECT_SOURCE_DIR}/../../cmake/TrailMateLinuxSources.cmake") trailmate_add_linux_common(trailmate_cardputer_zero_linux_common) trailmate_add_linux_ui_shell(trailmate_cardputer_zero_ui_shell trailmate_cardputer_zero_linux_common) ``` 注意: - `FetchContent` 的 SDL3 只应在 simulator target 中出现。 - `FetchContent` 的 LVGL 可以由 shared helper 提供,但 SDK path 不一定走这个 LVGL。 - `apps/linux_rpi` CMake path 可以继续使用 fetched LVGL,SDK SCons path 用 SDK LVGL。 - 不要让 root `CMakeLists.txt` 被 Linux 目标劫持;root 仍有 ESP-IDF 历史职责。 ### 7.2 LVGL LVGL 适配最容易失控,必须明确 owner。 规则: - 一个进程中 `lv_init()` 只能由一个 host layer 调。 - 一个 display 的 buffer 和 flush callback 只由创建者拥有。 - `ui_shared` 创建 object graph,不拥有 Linux display。 - `ShellSession` 只管 Trail Mate session 生命周期,不管 OS display。 - simulator 可以通过 Canvas host 把 LVGL RGB565 buffer 拷到 SDL shell。 - 真机 SDK path 应用 native LVGL display,不走 Canvas copy。 需要避免: - 在 `ui_shared` 中 include Linux headers。 - 在每个页面里写 Linux 特例。 - 在 SDK path 再套一层 Canvas,导致性能和职责都变差。 ### 7.3 输入 当前 shared shell 输入语义是 `InputEvent` 到 LVGL key 的映射。后续保持这个模型即可。 实现建议: - simulator:继续 SDL event -> `InputEvent`。 - custom framebuffer path:evdev -> `InputEvent` -> `ShellSession`。 - SDK native path:优先用 LVGL evdev;如需要统一行为,再引入 evdev -> `InputEvent`。 输入 adapter 要解决: - key down/up/repeat。 - modifier。 - printable ASCII。 - back/home/next/power 语义。 - 无输入设备时的 graceful degradation。 ### 7.4 存储 所有 Linux storage 都应走统一 root resolver。 建议约定: | 用途 | env override | 默认 | | --- | --- | --- | | settings/state | `TRAIL_MATE_SETTINGS_ROOT` | `$HOME/.trailmate_cardputer_zero` 或 Windows `%APPDATA%` | | SD-like files | `TRAIL_MATE_SD_ROOT` | settings root 下的 `sdcard` | | packs | `TRAIL_MATE_PACK_ROOT` | repo root 下 `packs` | | GPS NMEA file | `TRAIL_MATE_GPS_NMEA_FILE` | 无 | | GPS serial | `TRAIL_MATE_GPS_DEVICE` | 无自动打开 | 文件写入: - 小文件使用 temp file + fsync + rename。 - settings namespace 文件名必须 sanitize。 - blob 建议继续 hex 编码或迁移到明确 binary file,不能混入非转义分隔符。 - 删除动作必须 root-contained。 ### 7.5 GPS 当前 `gps_runtime.cpp` 已经有不错的基础: - env 默认位置。 - NMEA RMC/GGA/GSA/GSV 解析。 - Linux serial nonblocking read。 - file source incremental read。 - stale detection。 - GNSS satellite snapshot。 后续改进: - 把 NMEA parser 拆成可单测 helper。 - 增加 checksum 严格模式配置。 - 增加 serial reconnect backoff。 - 增加 device discovery,但默认不要随便打开所有 `/dev/tty*`。 - 真机 UI 应显示 source:simulated/env/file/serial/stale。 ### 7.6 Hostlink 当前 hostlink TCP runtime 已经可用,但需要调整默认安全姿态。 建议: - simulator 默认 bind `127.0.0.1`。 - 真机要对外监听时必须显式设置 `TRAIL_MATE_HOSTLINK_BIND=0.0.0.0`。 - endpoint file 写入 SD root 下 `hostlink/endpoint.txt` 可以保留。 - 后续如果承载敏感操作,需要认证或 pairing,不要只靠局域网隔离。 ### 7.7 LoRa、Walkie、SSTV 当前这三块主要是 synthetic runtime: - LoRa:生成 RSSI 曲线。 - Walkie:生成 tx/rx level。 - SSTV:生成 frame 并保存 PPM。 短期保留价值: - 帮助 UI 页面和 flow 在 Linux 上可验证。 - 支撑 simulator 演示。 必须补的边界: - status 显示 simulated。 - 真机默认不要宣称可用 radio/audio。 - 真实 radio/audio 接入前,不要让上层 protocol 依赖这些 synthetic 行为。 ### 7.8 Team 和 Chat 当前 Linux facade 已经让 chat/contact/team 在 simulator 中很有生命力,但这部分最需要防止概念漂移。 要拆清楚: - `core_chat` 的 domain/usecase/infra store 是可复用核心。 - Linux loopback mesh 是 simulator adapter。 - demo contacts 是 simulator data seed。 - team dummy crypto 是 demo-only。 - team UI store 中内存 snapshot 与 GPX append 是早期 runtime,不等于真实团队持久化完整实现。 真实设备前必须做: - 替换 dummy crypto。 - 把 demo seed 变成 opt-in。 - 明确 mesh transport:LoRa、BLE、hostlink、local loopback 各是什么。 - 让 contact/node store 的持久化格式有版本和迁移策略。 ### 7.9 Wi-Fi、USB、FOTA、Orientation 这些目前多数是 unsupported 或空实现。正确策略是继续显式 unsupported,直到有真实 Linux path。 不要为了菜单完整而返回 `true`。 UI 应根据状态显示不可用,而不是进入半功能页面。 ### 7.10 线程和生命周期 Linux common 中已经有 hostlink worker thread。后续新增线程时遵守: - `start()` 幂等。 - `stop()` 必须 join。 - 析构不抛异常。 - 后台线程不能调用 LVGL。 - shared state 用 mutex/atomic 明确保护。 - 测试要覆盖 start-stop-start。 ### 7.11 错误处理 原则: - app shell main 可以 catch exception 并返回非 0。 - platform runtime 合约尽量返回 `Status` 或 bool + message。 - 真机设备路径失败要可诊断:输出设备路径、errno、fallback。 - simulator 脚本失败要给安装建议。 ### 7.12 环境变量命名 继续使用 `TRAIL_MATE_` 前缀。 建议规则: - runtime root:`TRAIL_MATE_SETTINGS_ROOT`、`TRAIL_MATE_SD_ROOT` - simulator:`TRAIL_MATE_SIM_*` - GPS:`TRAIL_MATE_GPS_*` - hostlink:`TRAIL_MATE_HOSTLINK_*` - demo:`TRAIL_MATE_DEMO_*` - device:`TRAIL_MATE_FBDEV`、`TRAIL_MATE_INPUT_DEVICE` 环境变量是启动配置和测试注入,不应成为业务持久化的唯一来源。 ## 8. 具体 backlog ### 8.1 立即做 1. 修复当前 compile gate。 验收:simulator common、UI shell、path safety smoke、rpi framebuffer target 都能编译。2026-05-08 这轮已经修掉 LVGL callback 签名和 `evdev_input.cpp` 的 `kKeyMap` 初始化风险,下一步应直接跑 Linux/WSL 下的完整 CMake build。 2. 稳定 Linux CMake source helper。 验收:sim/rpi CMake 不再重复大 source list;rpi-only source 有清晰入口;添加 shared UI 源文件时只改一处。 3. 回归 `ShellSession` 与 LVGL input 细节。 验收:simulator 行为不变;单个 key enqueue 能产生 press/release;`continue_reading` 不吞事件;SDK path 可以复用 session 而不被 `lv_init()` 所有权冲突阻塞。 4. 完成 `platform/linux/rpi` evdev input adapter。 验收:custom framebuffer shell 能通过键盘导航;`TRAIL_MATE_INPUT_DEVICE=/dev/input/eventX` 可覆盖;无输入设备时只降级并打印诊断;Cardputer Zero 内置键盘 Fn/组合键映射来自真机事件码采样。 5. 扩大 runtime path helper 覆盖。 验收:settings、route、tracker、sstv、team、hostlink、map tiles、pack repository 共用路径解析;absolute path、`..`、跨 root 删除都被拒绝;map/hostlink/team/SSTV 的旧 env 常量和 `runtime_paths.h` 中不准确的 fsync 注释被清掉。 6. 修复并纳入 path traversal tests。 验收:absolute path、`..`、跨 root 删除都被拒绝。 7. 建立 capability truth table 并更新 UI status。 验收:`capability_status.h` 出现在 contract inventory;LoRa/Walkie/SSTV 等 public header 继续暴露 `capability_status()`;至少一个 UI 状态入口消费该 API;synthetic 功能不再被展示为真实支持。 8. 让 runtime mode 真正控制 facade composition。 验收:`TRAIL_MATE_RUNTIME_MODE=demo/local/mesh` 能改变 demo seed、loopback mesh、dummy crypto、真实设备能力呈现;默认真机不启用 demo world,也不默认组合 dummy crypto 和 loopback pairing。 9. 验证 simulator runtime mode 默认值写入语义。 验收:POSIX 和 Windows 都只在 `TRAIL_MATE_RUNTIME_MODE` 未设置时写入 `demo`;用户显式设置 `local` 或 `mesh` 时不会被 simulator main 覆盖。 ### 8.2 第一轮真实设备闭环 1. 把 `apps/linux_rpi/main/src/main.cpp` 从 bring-up UI 改为 shared shell。 2. 保留 `LV_LINUX_FBDEV_DEVICE` 和 `/proc/fb` auto-detect。 3. 保留 `LV_LINUX_KEYBOARD_DEVICE`,补更稳健的 input detection。 4. 记录启动日志:display path、input path、settings root、sd root、capability mode。 5. 在真机上验证 boot/menu/settings。 验收: - SDK path 构建成功。 - 真机显示 shared shell。 - 真机可按键操作。 - Settings 持久化。 ### 8.3 第一轮功能迁移 优先顺序: 1. Settings 2. Contacts/Chat local store 3. GPS page with NMEA source status 4. Tracker file workflow 5. Hostlink page 6. Team local runtime cleanup 7. Map tile/file storage 暂缓: - real LoRa - real walkie/audio - Wi-Fi provisioning - USB mass storage - firmware update - BLE phone integration ### 8.4 CI 和验证增强 新增建议: - Linux CMake source helper smoke。 - path safety test。 - NMEA parser unit tests。 - hostlink start/stop/restart test。 - capability status test。 - optional Docker image build check。 - optional SDK build job,需要能缓存或固定 SDK 获取方式。 当前已有命令: ```bash python3 scripts/check_platform_ui_boundaries.py cd builds/linux_cmake cmake --preset linux-simulator-debug cmake --build --preset linux-simulator-debug-build ctest --preset linux-simulator-debug-test cmake --preset linux-uconsole-debug cmake --build --preset linux-uconsole-debug-build ctest --preset linux-uconsole-debug-test cmake --preset linux-uconsole-release cmake --build --preset linux-uconsole-deb ``` Windows/WSL simulator: ```powershell wsl.exe --exec bash -lc 'cd /mnt/c/Users/VicLi/Documents/Projects/trail-mate/builds/linux_cmake && cmake --preset linux-simulator-debug && cmake --build --preset linux-simulator-debug-build && ctest --preset linux-simulator-debug-test' ``` Windows/WSL uConsole: ```powershell wsl.exe --exec bash -lc 'cd /mnt/c/Users/VicLi/Documents/Projects/trail-mate/builds/linux_cmake && cmake --preset linux-uconsole-debug && cmake --build --preset linux-uconsole-debug-build && ctest --preset linux-uconsole-debug-test' ``` SDK device: 当前仓库没有活跃的 Cardputer Zero Linux CI 入口。该目标需要补齐独立 device shell、SDK build 入口和硬件验证 gate 后,再作为专门命令列入这里。 ## 9. 代码审查清单 每个 Linux 适配 PR 都应该检查: - 是否新增了 `modules/core_* -> platform/*` 依赖。 - 是否新增了 `modules/ui_shared -> platform/linux/*` 依赖。 - 是否把 simulator-only 代码放进了 `platform/linux/common`。 - 是否把 Pi-specific 代码放进了 `platform/linux/common`。 - 是否新增了无解释的 `#ifdef __linux__`。 - 是否新增了可被路径穿越影响的文件操作。 - 是否把 synthetic runtime 标成 supported。 - 是否让 app shell 长出业务逻辑。 - 是否更新了 CMake shared source helper。 - 是否有至少 smoke 或 unit test。 - 是否更新了能力状态和文档。 ## 10. Definition of Done Linux 适配不能只以“能编译”作为完成标准。 一个 Linux slice 完成,至少满足: - 合约位置正确。 - 实现位置正确。 - simulator 能跑。 - rpi CMake target 不退化。 - 如涉及真机,SDK path 有验证路径。 - 能力状态诚实。 - synthetic/demo 行为不会默认污染真实设备。 - 文件路径安全。 - 生命周期可停止、可重启、无明显泄露。 - CI 或手动验证命令明确。 - 文档同步。 整个 Cardputer Zero Linux 适配真正可称为完成时,应满足: - `apps/linux_sim` 是稳定开发和验证工具。 - `apps/linux_rpi` SDK path 是真实设备主路径。 - `platform/linux/common` 不含 simulator/device 特例。 - `platform/linux/rpi` 只含真机特有适配。 - shared shell 在 simulator 和真机上是同一套。 - Settings、Chat/Contacts、GPS、Tracker、Hostlink 至少有真实 Linux runtime。 - LoRa/Walkie/SSTV/Wi-Fi/USB/FOTA 等能力要么真实可用,要么诚实 unsupported/simulated。 - `modules/core_*` 与 `modules/ui_shared` 边界由自动检查守住。 - 新增 Linux 设备族不需要复制现有 app shell。 ## 11. 推荐路线图 ### Milestone A:结构收口 目标:减少重复和过期文档。 - 抽 CMake source helper。 - 更新 app-local specs。 - 建立 path/capability helper。 - 增加 path safety tests。 ### Milestone B:真机 shell 收口 目标:让 SDK path 跑 Trail Mate shared shell。 - 拆 ShellSession。 - SDK main 接 shared shell。 - evdev input 可用。 - 启动日志清晰。 ### Milestone C:Settings 验证 slice 目标:验证最小真实功能闭环。 - Settings 页面在 simulator/device 都可用。 - settings persistence 真正落盘。 - capability status 在 UI 中可信。 ### Milestone D:数据和通信基础 目标:让 Linux 设备不只是 UI demo。 - Chat/contact/node store 整理。 - Hostlink 安全默认值。 - GPS NMEA serial 真机验证。 - Tracker/route 文件工作流。 ### Milestone E:硬件能力 目标:逐步接真实设备能力。 - Battery/power。 - Brightness/backlight。 - Audio/walkie。 - Radio/LoRa。 - Wi-Fi。 - USB/FOTA,如设备和产品形态需要。 ### Milestone F:发布与维护 目标:从开发 shell 进入可用产品线。 - systemd service 或启动脚本。 - 配置目录和权限策略。 - 日志策略。 - release artifact。 - 设备回归手册。 ## 12. 当前最终判断 当前 Linux 适配已经完成了最难的第一件事:把目标结构和边界意识立起来,并让 simulator 与 Linux common runtime 具备复用共享 UI 的基础。根据 2026-05-08 最新回归,代码还进一步做了 CMake helper、LVGL session/host 分层、evdev 输入、runtime path/env、capability status 上移与部分 public API、demo world 抽取、SDK bring-up 强化等正确方向的结构推进。 但这轮推进还没有到“适配完成”。下一阶段的关键不是继续堆更多模拟功能,而是先把这批结构改动收口成可构建、可测试、可运行的事实: - 先修 compile gate,让 sim/rpi/test 都重新绿。 - 再验证 LVGL input callback,让按键事件语义可靠。 - 再收口真机输入/display 主路径,特别是 rpi CMake、SDK main、evdev 真机验证。 - 再收口 path/capability/runtime 配置,让 pack repository 路径不再游离,让 capability contract inventory 和 UI 呈现跟上。 - 再收口 demo 与真实设备 facade 的边界,让真实设备默认不继承模拟世界。 如果按这个顺序推进,Linux 线会自然长成 Trail Mate 的一等平台,而不是新的平行项目。