Agents: start at
docs/AGENT_BRIEF.md, then your phase document underdocs/phases/. Each phase document opens with a Start here block giving its branch, worktree command, dependencies and definition of done — it is a complete work order.This document is the index and the source of the project-wide constraints (§5).
docs/HARDWARE_FINDINGS.mdis normative measured fact.
Build a 3D indoor positioning system from ESP32-C3 boards using Wi-Fi FTM (Fine Timing Measurement).
- One initiator (mobile node) measures its distance to several anchors (stationary FTM responders).
- Distances plus known anchor positions give a 3D fix by trilateration.
- Target accuracy is not demanding — the FTM hardware quantises to 15 cm and that is explicitly acceptable for this project. This is a feasibility bench, not a metrology instrument.
- Currently available hardware: 2× ESP32-C3. Everything must be designed for N anchors and validated with 2. Never hardcode a two-board assumption.
| Item | Status |
|---|---|
ESP-IDF v5.5.2 at C:\Users\murilo\esp\v5.5.2 |
Working |
| Board A — COM3, ESP32-C3 | Working, used as responder |
Board B — COM4, ESP32-C3 rev v0.4, MAC 14:63:93:8d:96:e4 |
Working, used as initiator |
| Espressif FTM console example | Built, flashed, end-to-end FTM verified |
| Measured baseline at ~1.2 m separation | mean 1.22 m, 28–30/30 valid readings |
| Bench fixture: boards fixed 1.00 m apart | Standing setup — tests may assert 1 m ground truth |
| Git repository | Initialised, branch main |
Docker Desktop 4.82.0 + ftm-dev image |
Installed; firmware build, Ceedling, IDF linux target all verified |
| Python venv + test libs | Installed and verified |
All hardware facts are in docs/HARDWARE_FINDINGS.md. That document is
normative — do not re-derive these numbers, and do not contradict them without
new measurements.
ftm_measurement/
├── docs/
│ ├── PLAN.md <- this file
│ ├── AGENT_BRIEF.md <- agent onboarding; read before any phase
│ ├── HARDWARE_FINDINGS.md <- verified measurements, normative
│ ├── ARCHITECTURE.md <- layering, swappable modules, contracts
│ ├── RTOS.md <- tasks, queue, overflow policy
│ ├── TESTING.md <- test framework + worst-case catalogue
│ ├── CONTAINER.md <- container (build + host unit tests only)
│ ├── PROTOCOL.md <- ftmbin wire format (one serialiser)
│ ├── WORKFLOW.md <- git rules, merge gate, reports
│ └── phases/
│ ├── PHASE_0_test_infra.md
│ ├── PHASE_1_bench_firmware.md
│ ├── PHASE_2_calibrator.md
│ ├── PHASE_3_definitive_firmware.md
│ ├── PHASE_4_protocol.md
│ ├── PHASE_5_positioning.md
│ └── PHASE_6_e2e_and_manual.md
├── tools/
│ ├── bench_firmware/ <- Phase 1: board validation firmware
│ ├── bench/ <- Phase 1: host validation scripts
│ └── calibrator/ <- Phase 2: firmware + tkinter UI
├── components/ <- Phase 3: the definitive layered firmware
│ ├── domain/
│ ├── services/
│ └── drivers/
├── docker/ <- Phase 0: build/test container
└── tests/ <- all committed tests (WORKFLOW.md §4)
There is deliberately no ESP-IDF project at the repository root. Phase 1
moved the bench example into tools/bench_firmware/; the definitive firmware
(Phase 3) lives in components/ with its own project. A root main/ would be
ambiguous about which firmware it belongs to.
Phases are ordered by dependency. Each is independently assignable.
| Phase | Deliverable | Depends on | Doc |
|---|---|---|---|
| 0 ✅ | Container + test infrastructure — merged | — | PHASE_0 |
| 1 ✅ | Bench validation firmware in tools/ — merged |
— | PHASE_1 |
| 2 | Calibration firmware + tkinter UI → CSV | 1 | PHASE_2 |
| 3 | Definitive layered firmware | 0, 2 | PHASE_3 |
| 4 | Binary protocol + .ftmlog logging + replay |
3 | PHASE_4 |
| 5 | Positioning — host-first from logs, then target | 3, 4 | PHASE_5 |
| 6 | E2E suite + human-in-loop movement test | 3, 4, 5 | PHASE_6 |
Phases 0 and 1 have no dependencies and may run in parallel.
Phase 4 exists so Phase 5 can be done offline. The protocol, .ftmlog
capture and replay tooling let the 3D algorithm be developed and proven on the
host against recorded real measurements — with their real noise and drift —
before any of it is compiled for the target. Phase 5 is then split: 5a host,
5b target, one shared source file.
The container and all toolchains are installed and validated
(docs/CONTAINER.md §8).
These apply to every phase. An agent violating one should stop and flag it.
- Every feature has unit tests covering worst-case behaviour, not just the
happy path. See the worst-case catalogue in
docs/TESTING.md§4. A PR with only happy-path tests is incomplete. - Every feature is exercised end-to-end on real hardware. Both boards are permanently connected on COM3 and COM4. E2E tests run autonomously.
- The layering in
docs/ARCHITECTURE.mdis mandatory, including layers that are currently empty (drivers/devices/). Empty layers exist for standardisation and get aREADME.mdexplaining why they are empty. - Swappable modules follow one pattern — interface vtable, Kconfig-selected
implementation, shared contract suite (
docs/ARCHITECTURE.md§2). This covers role selection and serialisation. No#ifdefscattered through business logic. - Serialisation is a swappable module, consuming domain snapshots and
producing bytes. It must not include a driver or ESP-IDF header, must not
allocate, and must not perform I/O. Every implementation passes the shared
contract suite (
docs/TESTING.md§4.8). - The measurement task never blocks on transmission (
docs/RTOS.md). On queue overflow the oldest entry is discarded so the freshest reading always survives — this is a navigation system, and stale data is worse than no data. Loss is made observable by amsg_seqgap plus adropped_totalcounter, both diagnostic only: navigation uses current data and never waits for or reconstructs missing messages. - Design for N anchors. Two boards is the current test fixture, not the
architecture. With fewer than 4 usable anchors the system reports
RANGE_ONLY— per-station distances — and never fabricates a 3D fix. - Fix status travels with the data. Every cycle sends
NAV-STATUSwithfix_qualityandnum_anchors. Absence is information: noNAV-POSITIONmessage means no position. Never emit a position message with placeholder or zeroed coordinates — zero is a valid coordinate (docs/PROTOCOL.md). - Never hardcode calibration constants in logic. They come from the generated calibration table (Phase 2 output).
- Builds and host unit tests run in the container; flashing, E2E and manual
tests run on Windows inside the project venv (
docs/CONTAINER.md). Neverpip installinto the system or IDF Python. The container never touches the boards. - Branch per feature;
mainis never committed to directly. Merges are human-approved after all tests pass and the results have been shown. - Never weaken a pre-existing test to make your change pass. If you break
a test you did not write, fix your implementation or stop and ask. This is
the regression rule in
docs/WORKFLOW.md§3 and it is absolute. - Every branch ends with a work report at
docs/reports/<branch>.mdstating deviations and scope changes, and every commit carries theCo-Authored-Bymodel trailer (docs/WORKFLOW.md§5–6).
Once Phase 0 is done, tools/dev.ps1 routes every command to the right place —
container or host — and an agent should not need to think about which:
.\tools\dev.ps1 build # container
.\tools\dev.ps1 test-host # container: Ceedling + IDF linux target
.\tools\dev.ps1 coverage # container: gcovr
.\tools\dev.ps1 flash responder # Windows host
.\tools\dev.ps1 e2e # Windows host, venvThe Windows host has ESP-IDF v5.5.2 at C:\Users\murilo\esp\v5.5.2, used for
flashing and E2E.
export.ps1 fails from a plain PowerShell session because the Microsoft Store
python alias shadows the IDF Python. Always prepend the IDF Python first:
$env:Path = "C:\Users\murilo\.espressif\tools\idf-python\3.11.2;" + $env:Path
& "C:\Users\murilo\esp\v5.5.2\esp-idf\export.ps1"Shell state does not persist between tool calls — chain the export with the command, or use a wrapper script.
Console must be on USB-Serial-JTAG. These boards expose only the built-in
USB-Serial-JTAG (VID 303A / PID 1001), not a UART bridge. Any firmware in
this repo must set:
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
Without it the REPL is unreachable and the failure is misleading: boot logs still appear on the COM port, but every write times out.
- Read your phase doc fully before starting. It lists preconditions, deliverables with exact paths, required tests, and acceptance criteria.
- Do not expand scope. If a phase doc omits something you think is needed,
record it in your work report (
docs/WORKFLOW.md§6) rather than building it. - Record measurements, don't assume them. If you take a new hardware
reading that contradicts
docs/HARDWARE_FINDINGS.md, update that document and say so explicitly in your report. - Update the phase doc's status table when you finish.
- Report honestly. If a test fails or a step was skipped, say so with the output. Do not describe unverified work as verified.