18 KiB
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_mainonly 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
- Dependencies are one-way only:
app_composition -> control_plane -> domain -> platform. - Lateral direct dependencies are forbidden unless listed in
dependency_whitelist.md. - Cross-domain communication must use events or Port interfaces; never include another domain's
internal/*.h. - Each component keeps exactly one public facade header in
include/; private headers stay ininternal/viaPRIV_INCLUDE_DIRS.- Long-term canonical facade headers in this repo:
platform/include/platform.hdomain/include/domain.hcontrol_plane/include/control_plane.happ_composition/include/app_composition.h
- Upper layers may include only immediate downstream public facade headers; leapfrog includes across multiple layers are forbidden.
- Cross-cutting contracts must have a single declared owner layer and one public declaration point; duplicated declarations across layers are forbidden.
- Runtime state must have a single source of truth (SSOT); orchestration layers should query/subscribe rather than duplicate lower-layer lifecycle or readiness state.
- Blocking APIs must define timeout and idempotent semantics (no destructive background continuation after caller timeout).
- Facade contracts for commands/events must use strongly typed enums/structs; primitive
int/uintmagic values across facade boundaries are forbidden. - Non-owner facades must not expose downstream-layer public types directly (for example,
typedef platform_*indomain/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
typedefdownstream-layer public types; use implementation-layer mapping/adapters instead.
LCD Expression Management
- Use
docs/lcd-expressions.mdas the operational guide for managing, requesting, and adding LCD expressions. - New expression semantics must be declared as
domain_expression_*contracts incomponents/domain/include/domain.hand mapped in the domain runtime adapter catalog. - Upper layers must request expressions through
domain_runtime_request_expression(); they must not callplatform_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 byapp_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.pycommand, 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:
export ESPRESSIF_ROOT="${ESPRESSIF_ROOT:-$HOME/.espressif}" export IDF_VERSION_TAG="v5.5.4" - If
eimexists on the current machine, prefer running ESP-IDF commands througheim run, for example: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,
eimlogging initialization may fail if the default user state/log directories are not writable. In that case, pass--log-filewith a writable path, for example: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
eimdoes not exist, prefer the newer EIM-managed activation script when available: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:
export IDF_PATH="$ESPRESSIF_ROOT/${IDF_VERSION_TAG}/esp-idf" source "$IDF_PATH/export.sh" - If both
eimand activation scripts exist, prefereim runfirst. - 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 runfirst, activation script second, legacyexport.shflow last.
- Prefer path expressions that are portable across developer machines. Avoid hardcoded user-specific absolute paths such as
-
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_casefor functions and variables; use module-prefixed public APIs. - Use
UPPER_SNAKE_CASEfor macros/constants (CMD_SEND_DATA,WIFI_CONNECTED_BIT). - Prefix file-local statics with
s_(for examples_server,s_jobs) and keep per-fileTAGlogging 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
- Create feature branches from
develop; merge back via PR with squash or rebase (no merge commits ondevelop). - When
developis release-ready, cut arelease/<version>branch for stabilization; only bug-fixes go there. - After release QA, merge
release/*intomainwith a version tag, then back-merge intodevelop. - Hotfixes branch from
main, merge into bothmain(tagged) anddevelop. - 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
sdkconfigvariants, serial-port-specific commands, or generatedbuild/artifacts.