Files
lucky-cat/AGENTS.md
T

331 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Repository Guidelines
## Project Structure & Layering
- `components/platform/{include,internal,src}`: hardware/system abstraction only.
- `components/domain/{include,internal,src}`: business-domain services.
- `components/control_plane/{include,internal,src}`: lifecycle, policy, health, retry/backoff orchestration.
- `components/app_composition/{include,internal,src}`: composition boundary; `app_main` only wires modules.
- `dependency_whitelist.md`: approved exceptions for unavoidable lateral dependencies.
- `components/domain/third_party/`, `components/domain/assets/fonts/`, `tools/`: vendored code, embedded assets, utility scripts.
- `build/`: generated artifacts only (ignored).
## Mandatory Architecture Constraints
1. Dependencies are one-way only: `app_composition -> control_plane -> domain -> platform`.
2. Lateral direct dependencies are forbidden unless listed in `dependency_whitelist.md`.
3. Cross-domain communication must use events or Port interfaces; never include another domain's `internal/*.h`.
4. Each component keeps exactly one public facade header in `include/`; private headers stay in `internal/` via `PRIV_INCLUDE_DIRS`.
- Long-term canonical facade headers in this repo:
- `platform/include/platform.h`
- `domain/include/domain.h`
- `control_plane/include/control_plane.h`
- `app_composition/include/app_composition.h`
5. Upper layers may include only immediate downstream public facade headers; leapfrog includes across multiple layers are forbidden.
6. Cross-cutting contracts must have a single declared owner layer and one public declaration point; duplicated declarations across layers are forbidden.
7. Runtime state must have a single source of truth (SSOT); orchestration layers should query/subscribe rather than duplicate lower-layer lifecycle or readiness state.
8. Blocking APIs must define timeout and idempotent semantics (no destructive background continuation after caller timeout).
9. Facade contracts for commands/events must use strongly typed enums/structs; primitive `int`/`uint` magic values across facade boundaries are forbidden.
10. Non-owner facades must not expose downstream-layer public types directly (for example, `typedef platform_*` in `domain/include`); bridge/mapping belongs in implementation adapters.
## Contract Ownership and Declaration
- Each cross-cutting contract family (shared structs/enums/callback signatures/events) must have exactly one owner layer and one public declaration point.
- Non-owner layers must not redefine semantically equivalent contracts under new names.
- Cross-layer reuse should prefer owner types at owner boundaries, but non-owner facades must not leak downstream types; use explicit adapter mapping at implementation boundaries.
- Owner changes require an explicit architecture decision record and a staged migration plan.
## Canonical Contract Ownership Matrix (Normative)
The following matrix is authoritative for all new and refactored cross-layer contracts in this repository.
| Contract family | Owner layer | Single declaration point | Non-owner usage rule |
| --- | --- | --- | --- |
| Direction catalog, angle math, direction lookup (`domain_direction_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` consumes via `domain` facade only; do not duplicate equivalent structs/enums in upper facades. |
| Direction calibration policy/state model (calibration angle, sweep, search policy) | `domain` | `components/domain/include/domain.h` | Hardware wiring details must be bridged in implementation adapters, not copied into upper-layer public facades. |
| Connectivity semantic prompt IDs/events (connecting/success/fail/config) | `domain` | `components/domain/include/domain.h` | `platform` must not expose duplicated semantic enums in public facade; map semantics in adapters. |
| Mini-program Wi-Fi provisioning semantics (TalkingQ BLE discovery/GATT schema, credential events, status/device-info payloads) | `domain` | `components/domain/include/domain.h` | `platform` exposes generic BLE GATT server primitives only; `control_plane` owns Wi-Fi connection policy and worker lifecycle. |
| Hardware/runtime primitive descriptors (GPIO digital input config, low-level actuator primitives, storage identity/blob KV APIs, wall-clock sync API) | `platform` | `components/platform/include/platform.h` | Upper layers call through owner facade or owner-backed adapters; no semantic redefinition in non-owner public headers. |
| Battery sensing and battery-state policy (`platform_battery_*`, `domain_battery_*`) | `domain` for SOC/level semantics; `platform` for ADC, charger GPIO, and load-quality primitives | `components/domain/include/domain.h` for semantic state; `components/platform/include/platform.h` for hardware samples | `control_plane` consumes domain status only; platform load tracking stays internal and upper layers must not infer SOC from raw ADC values. |
| LAN HTTP transport contract (bounded request/response structs, handler delegate, server lifecycle/state) | `platform` | `components/platform/include/platform.h` | `domain` owns route semantics and maps them in implementation; upper layers must not expose platform HTTP types. |
| Multimodal session transport contract exposed to orchestration (connected/disconnected/error/text/binary event family, audio uplink callback family) | `domain` | `components/domain/include/domain.h` | `platform` internal transport types stay internal; cross-layer public contract is declared once at `domain` facade. |
| Device-auth persistence port (opaque auth key/state record slots and legacy migration targets) | `domain` | `components/domain/include/domain.h` | `control_plane` owns serialized auth record schemas and consumes typed auth slots through `domain`; `platform` remains an opaque blob KV owner only. |
| User birth profile and daily fortune result (`domain_birth_profile_*`, `domain_fortune_result_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` consumes and persists these contracts through the domain facade; platform storage remains opaque and semantics-free. |
| LAN user configuration web application contract (`domain_user_config_server_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` owns connectivity-driven start/stop policy; page routes and user/fortune JSON remain domain-owned. |
| LCD expression semantic catalog and trigger request contract (`domain_expression_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` requests semantic expressions through `domain`; `platform` exposes playback primitives only and must not duplicate semantic expression IDs publicly. |
| LCD presentation playback primitives (asset sequence/slot reels/idle playback request) | `platform` | `components/platform/include/platform.h` | Upper layers must use `domain` expression mapping; playback asset directories and procedural presentation details are bridged in implementation adapters. |
| Connectivity orchestration policy contract (bootstrap flow, retry/backoff/fallback policy APIs) | `control_plane` | `components/control_plane/include/control_plane.h` | `app_composition` triggers these policies but must not implement policy logic. |
| Daily fortune fetch/apply orchestration policy (`control_plane_fortune_*`) | `control_plane` | `components/control_plane/include/control_plane.h` | `app_composition` starts control-plane workers only; birth-profile storage, direction semantics, and transport primitives remain in their owner layers. |
| Model-command and multimodal runtime orchestration contract (model command type/callback, runtime state snapshot) | `control_plane` | `components/control_plane/include/control_plane.h` | Non-owner layers subscribe/query through `control_plane` APIs; avoid duplicated runtime state mirrors. |
| App startup wiring contract (boot profile, startup sequence entrypoints) | `app_composition` | `components/app_composition/include/app_composition.h` | `main/app_main` calls composition entrypoints only; policy and workers must remain below composition. |
Additional enforcement rules:
- Any new cross-layer contract must map to one matrix row above before code is merged.
- If a new contract family does not fit existing rows, add an ADR first, then update this matrix in the same PR.
- Public facade headers must not `typedef` downstream-layer public types; use implementation-layer mapping/adapters instead.
## LCD Expression Management
- LCD/GFX 开发先阅读 `docs/GFX表情显示应用与开发指南.md`。旧 `docs/lcd-expressions.md` 的测试图案和优先级仲裁描述不能作为当前实现事实;以现行源码和新指南为准。
- New expression semantics must be declared as `domain_expression_*` contracts in `components/domain/include/domain.h` and mapped in the domain runtime adapter catalog.
- Upper layers must request expressions through `domain_runtime_request_expression()`; they must not call `platform_lcd_*` playback APIs directly.
- Platform LCD code must expose playback primitives only and must not reference `DOMAIN_EXPRESSION_*` semantic IDs.
- Asset-backed expressions should use the platform-owned direct LCD asset path documented in `docs/lcd-expressions.md`; custom playback order belongs in the domain catalog, not in duplicated asset files or control-plane logic.
- Do not add one-off expression APIs for each emotion/state unless an ADR explicitly justifies a new contract family.
## Concurrency and Callback Safety
- Any mutable runtime state accessed by multiple tasks/ISRs/callback contexts must be protected by explicit synchronization (mutex/spinlock/task-serialized model).
- Callback registrations (`cb + user_data`) must be read/written under synchronization and invoked outside locks using snapshotted copies.
- Public state-query APIs should return immutable snapshot structs, not pointers/references to mutable internal state.
- When lock acquisition can block, code must use bounded waits and return explicit failure on lock timeout.
## Managed Worker Ownership
- Every long-running worker/task/timer must have one explicit owner component and a documented lifecycle (`create/start/stop/join`).
- Stop paths must request cooperative cancellation first, then perform bounded wait/join, and finally apply deterministic fallback (for example force-delete) if timeout expires.
- Owner modules must clear task handles/state deterministically on worker exit to avoid stale lifecycle signals.
- Composition layer may create bootstrap wiring tasks, but policy workers must be owned by `control_plane` (or lower owner layer), not by `app_composition`.
## Runtime Composition Boundary
- Composition boundary owns application wiring: startup sequence, task graph assembly, and inter-module boot orchestration.
- Policy/orchestration layers own policy logic and coordination rules, but not top-level application boot wiring.
- Entry code (`app_main`) should only invoke composition-level entrypoints.
## Blocking and Lifecycle Contracts
- Any potentially blocking API must define timeout semantics, return behavior, and side effects after timeout.
- Default timeout behavior must be explicit and documented (including non-positive timeout handling).
- Start/Stop APIs must be idempotent and safe to call repeatedly.
- Stateful modules should expose lifecycle query APIs (for example, running/started status) instead of duplicating state in higher layers.
- Stop semantics must include real worker/resource teardown with bounded wait and deterministic fallback behavior.
- Unbounded waits in lifecycle/orchestration paths should be avoided unless explicitly justified and safe.
- Long-running loops in managed components must support cooperative termination.
## Build, Flash, and Validation
- Use the ESP-IDF/toolchain version declared by the project documentation or lockfiles.
- Activate ESP-IDF v5.5.4 before building.
- Before running any `idf.py` command, first determine which activation style exists in the current environment:
- Prefer path expressions that are portable across developer machines. Avoid hardcoded user-specific absolute paths such as `/home/<user>/...` or `/Users/<user>/...` in docs, scripts, and shell snippets when a variable-based form is sufficient.
- For POSIX shells, prefer parameterized paths such as:
```bash
export ESPRESSIF_ROOT="${ESPRESSIF_ROOT:-$HOME/.espressif}"
export IDF_VERSION_TAG="v5.5.4"
```
- If `eim` exists on the current machine, prefer running ESP-IDF commands through `eim run`, for example:
```bash
eim run <COMMAND> [IDF_VERSION]
eim run "idf.py build" v5.5.4
eim run "idf.py --version"
```
- In sandboxed or other write-restricted environments, `eim` logging initialization may fail if the default user state/log directories are not writable. In that case, pass `--log-file` with a writable path, for example:
```bash
eim --log-file ./eim-build.log run "idf.py build" v5.5.4
eim --log-file /tmp/eim.log run "idf.py --version" v5.5.4
```
- If `eim` does not exist, prefer the newer EIM-managed activation script when available:
```bash
source "$ESPRESSIF_ROOT/tools/activate_idf_${IDF_VERSION_TAG}.sh"
```
- If the EIM activation script does not exist, fall back to the legacy ESP-IDF export flow:
```bash
export IDF_PATH="$ESPRESSIF_ROOT/${IDF_VERSION_TAG}/esp-idf"
source "$IDF_PATH/export.sh"
```
- If both `eim` and activation scripts exist, prefer `eim run` first.
- Do not assume one activation path universally applies; verify the current machine/setup first.
- On non-POSIX environments, use the equivalent shell-native path syntax, but keep the same decision order: `eim run` first, activation script second, legacy `export.sh` flow last.
- `idf.py set-target esp32s3`: one-time target setup.
- `idf.py build`: compile and validate.
- `idf.py -p <PORT> flash monitor`: flash and open serial monitor (`Ctrl+]` to exit).
- `idf.py menuconfig`: adjust project options.
- `idf.py fullclean && idf.py build`: clear stale artifacts.
## Coding Style & Naming Conventions
- Use 4-space indentation and keep braces/function style consistent with existing `components/*/*.c`.
- Prefer `lower_snake_case` for functions and variables; use module-prefixed public APIs.
- Use `UPPER_SNAKE_CASE` for macros/constants (`CMD_SEND_DATA`, `WIFI_CONNECTED_BIT`).
- Prefix file-local statics with `s_` (for example `s_server`, `s_jobs`) and keep per-file `TAG` logging constants.
## Language Policy
Developer-facing artifacts may be written in **Chinese or English**. This includes:
- Source code: identifiers, comments, log tags, and string literals used for diagnostics.
- Version control: branch names, commit messages, PR titles and descriptions, and code-review comments.
- Documentation: READMEs, architecture docs, inline API docs, and this file (`AGENTS.md`).
When modifying existing artifacts, prefer the language already used in the surrounding content unless the task explicitly requires another language. Keep identifiers consistent with the existing codebase and use clear, unambiguous terminology.
**User-facing content:** UI strings, end-user help text, localization resource files, and any material rendered directly to end users may use the target locale language.
## Testing Guidelines
There is no dedicated unit-test directory currently. Minimum validation:
- Build check: `idf.py build`.
- For architecture-impacting PRs, also run focused guard checks:
- Verify no duplicated cross-cutting contract families across facades.
- Verify no new unbounded wait is introduced in lifecycle/orchestration paths.
- Verify application boot wiring remains in the composition boundary.
- Verify facade APIs do not use primitive magic command/event IDs in public callback signatures.
- Verify non-owner facade headers do not expose downstream-layer types directly.
- Verify new managed workers have explicit stop control and bounded teardown path.
- Add lightweight static checks (`rg`, linters, or scripts) as needed to enforce these constraints.
## Branching Strategy (GitFlow)
This project follows GitFlow. All work happens on topic branches; direct commits to `main` or `develop` are forbidden.
### Long-lived branches
| Branch | Purpose | Receives merges from |
| ------- | ------- | -------------------- |
| `main` | Production-ready releases; every commit is a tagged release. | `release/*`, `hotfix/*` |
| `develop` | Integration branch; latest delivered features for the next release. | `feature/*`, `release/*`, `hotfix/*` |
### Short-lived branches
| Branch pattern | Base | Merges back into | Naming example |
| -------------- | ---- | ----------------- | -------------- |
| `feature/<topic>` | `develop` | `develop` | `feature/ble-pairing` |
| `release/<version>` | `develop` | `main` **and** `develop` | `release/1.2.0` |
| `hotfix/<topic>` | `main` | `main` **and** `develop` | `hotfix/crash-on-boot` |
### Rules
1. Create feature branches from `develop`; merge back via PR with squash or rebase (no merge commits on `develop`).
2. When `develop` is release-ready, cut a `release/<version>` branch for stabilization; only bug-fixes go there.
3. After release QA, merge `release/*` into `main` with a version tag, then back-merge into `develop`.
4. Hotfixes branch from `main`, merge into both `main` (tagged) and `develop`.
5. Delete short-lived branches after merge.
## Commit & Pull Request Guidelines
Use a clear conventional format:
- Commit style: Strictly follow the Conventional Commits specification.
- Keep commits focused and atomic by layer/module (`platform`, `domain`, `control_plane`, `app_composition`).
- Source code is hosted in Gitea; submit changes through a Gitea Pull Request targeting the appropriate GitFlow branch.
- PRs include purpose, affected layer(s), whitelist changes (if any), config changes, and validation evidence.
- If work is executed from a staged remediation/audit checklist, remove completed checklist items in the same PR so the document stays a live source of truth.
## Security & Configuration Tips
- Never hardcode credentials or API keys in source files; configure through `menuconfig`.
- Do not commit local `sdkconfig` variants, serial-port-specific commands, or generated `build/` artifacts.
## GFX 表情开发约束(给 AI 与开发者)
本节是在原有架构规则上的补充,不替代前文。处理显示、资产、表情、缓存、
刷新或帧率任务时,必须先完整阅读 `docs/GFX表情显示应用与开发指南.md`,
再按任务核对当前源码和配置;不得把历史日志的资源开销或 FPS 当成当前事实。
涉及素材增删或预算时,还需核对 `docs/显示与音频资产台账.zh-CN.md`、构建白名单
及当前文件;台账是有日期的快照,不替代源码、manifest 和实际构建结果。
### 开始工作前
1. 确认仓库为 lucky-cat,检查分支、状态及已有差异。保留未提交代码、素材
和预览,不把 tq-cat 的实现或其他目录里的旧文件当成当前工程。
2. 优先查看 `lcd_direct.c`、领域表情目录、平台公共接口、构建列表和 Kconfig。
图层制作还需阅读 `split_idle_expression.py`、`compress_idle_expression.py`。
3. 当前是待机/listening GFX 与其他 PNG 混合后端;未接入 esp_emote_expression 状态机,
也未整体迁移到 LVGL。不要凭依赖存在或请求优先级字段推断功能已经实现。
### 必须保持的正确性
4. 保持控制层→领域层→平台层依赖方向。业务通过领域语义请求表情,GFX 类型
与资产实现留在平台私有代码,不增加跨层直接写 LCD 的入口。
5. GFX 锁必须由同一个任务获取和释放。批量对象修改、缓存捕获、性能计数
快照在渲染锁内完成;不能在初始化任务加锁、到渲染任务解锁。
6. 不在中断/定时器回调中生成纹理、解码、绘制、等待传输或打印性能日志。
保持 SPI 完成中断轻量,渲染任务和 GFX 刷新之间避免并行写屏。
7. DMA 暂存区必须等传输完成后才复用;整区域发送完成才通知 GFX。
超时后的在途缓冲不可立即覆盖或释放,不能绕过现有故障保护。
8. 自动返回待机只在没有新 pending 且没有停止请求时入队;不得覆盖刚来的
表情。取消与成功完成必须区分,修改完成事件前检查所有等待者。
9. 保留固定时间轴。每轮目标基于共同起点,不回退到画完再固定等 34ms,
不忙等、不自动跳过眼型,不未经验证修改全局 tick、SPI 时钟或任务优先级。
### 资源与缓存
10. 生成后的 C 数组不要手改。PNG 是可编辑输入,压缩脚本生成实际编译资产;
拆分脚本会覆盖图层,运行前检查美术是否有手工修改。
11. 当前九个缓存是带局部静态背景的不透明 RGB565 结果,不是纯透明眼睛。
换背景、鼻子、胡须、配色、眼睛位置或尺寸时,必须重建缓存并核对窗口。
12. 改时间线必须同步缓存表和回归测试;保持阶段不能产生多余刷新。
不支持的 closure 不得静默映射成别的眼型。
13. 网格锚点、最小边界位置补偿、闭眼弧线与眼睛中心属于耦合几何参数。
不得只替换资产就声称已适配;改尺寸需同时检查资源脚本的尺寸断言。
14. 原图为 RGB565 加独立 A8 平面,缓存为已经合成的 RGB565。验证颜色字节序、
透明层及交换配置,避免重复交换。新增区域裁剪必须恢复旧、新区域并集背景。
15. 新增表情先定义静态/动态层、缓存预算和结束行为,不批量无上限预热所有
表情。不得无授权删除未迁移 PNG、历史图层或未跟踪文件。
16. 缓存失败要释放部分成果并维持安全后备。当前资源随 LCD 常驻,停止循环
不等于完整销毁;新增热替换/重复初始化需要显式生命周期、超时和错误测试。
### 素材压缩、成本与视觉约束(强制)
- 新增或替换显示素材必须进行适合该格式的压缩优化及成本评估。根据 Flash、
解码耗时、PSRAM 和效果选择 PNG、RLE 或其他编码,不强制统一格式,不得
默认 GFX/RLE 比优化 PNG 更省空间,也不得仅凭“已压缩”认定整体优化完成。
- PNG 路径先读 `docs/lcd-image-asset-optimization.zh-CN.md`,从原图运行
`tools/optimize_lcd_assets.py generate`、`verify`;GFX 路径使用对应生成脚本
并做编码往返验证。有效 Alpha 不得删除,不能用普通色彩量化静默改变画面。
- 固定短动作优先保存少量关键资源,初始化生成局部缓存。避免保存大量全屏
中间帧;播放期间不得引入未经评估的重复解码、动态分配或整屏缓存复制。
- 交付必须比较原图文件、优化 PNG、未压缩像素、压缩载荷以及整个固件大小,
分别报告常驻/初始化峰值 PSRAM、静态数据/普通堆、内部 RAM 和 DMA 增量。
C 数组源文件的文本大小、文件系统分配空间、Flash 载荷不是同一个数字。
- 新方案比旧方案大时,必须说明成本增加、获益及证据等级;当前 listening
是空间换流畅度的已接受基线(固件增量 53,696 B、常驻像素 PSRAM 增量
423,234 B),不是所有表情的硬上限。不能按表情数量直接复制整套缓存。
超出该表情已确认预算、新增内部/DMA 缓冲,或可能影响 OTA/语音/网络时,
先报告预算、风险及替代方案,获得用户确认后实施;没有预算时先提出预算。
- 保持现有整体造型与画风,允许为性能做细微适配;改变角色造型、表情语义、
大幅改配色或动作方向前需用户确认。默认向中间闭眼,保留表情自身弧线;
不直接复制另一表情的几何参数,不通过两张完整眼型淡化制造双轮廓/高光重影。
- 视觉验收必须检查整体上移、残影、色差、闪烁、眼睑跳变、窗口截断及 HUD
遮挡,同时核对切换/取消/后备。用户观感确认要记入指南,但不能代替实测 FPS、
峰值内存或并行负载日志。资产台账需同步分类与大小;清理旧资源须遵循指南
的迁移完成条件,并另获授权,不得因目录名含 generated 就擅自删除。
### 状态、测量与验收
17. 时间沿用现有同步与时区服务;电量来自领域状态,不在显示层用电压重算。
保留未知值和二维码/边界测试不叠加规则。当前字形子集仅满足数字状态,不能
声称已支持任意中文文字。当前时间使用 Montserrat 14 数字子集直接合成,
电量只显示 40×20 电池素材,不创建 GFX label 或运行 LVGL GUI。
`tools/prepare_status_hud_assets.py --check` 验证字形提取及调色板游程往返;
HUD 资源 C 不手改,Alpha 不丢弃,不重复加入 PNG 素材包。
18. 性能测量沿用 `CONFIG_TQ_LCD_EMOTE_FPS_LOG`,不逐帧打印。区分步骤频率、
实际图像更新、DMA 块次数与面板扫描率;不将 30 FPS 配置写成实测 30 FPS。
19. 分项都是墙钟时间,含任务抢占;平均包含保持帧,各项最大值可能不在同一帧。
不把合成耗时当纯 CPU,不把等待传输当纯线上耗时。
20. 内存必须分别报告 Flash、PSRAM、内部 RAM 和 DMA。像素缓冲合计不是整个
组件总开销;应用镜像已包含嵌入式资源,不重复加算。留意联网/唤醒后的最低余量。
21. 变更后至少运行四个显示回归脚本和 ESP-IDF 5.5.4 构建。条件编译要验证
日志开启/关闭两种情况;涉及资源时检查编码往返及视觉预览。
22. 实机验收包括待机/表情切换、跨分钟、充电、缓存启动、唤醒与网络并行、
取消和超时。区分源码检查、主机测试、编译、设备日志与用户观感证据。
没有 TE 同步,不能承诺完全无撕裂;用户满意也不等于已量化稳定帧率。
回归命令(仓库根目录):
```bash
python3 tools/test_lcd_status_hud.py
python3 tools/test_lcd_emote_transport.py
python3 tools/test_lcd_emote_timeline.py
python3 tools/test_lcd_listening_gfx.py
```
提交只包含任务相关文件,遵守主题分支与原有提交规则;不要提交临时补丁脚本、
本机 sdkconfig、构建目录或未授权推送。修改当前机制时同步更新中文指南和测试。
纯文档任务只需检查 Markdown、链接、数字与当前代码的一致性,不为此重跑固件
构建或烧录;应明确本次验证层级,不能把之前的测试结果写成本次重新执行。