Thanks for helping improve AutoLee. This guide covers the project layout and how to build, test, and release the firmware.
| Path | Purpose |
|---|---|
main/ |
The firmware, grouped by concern (see below). |
lib/autolee_logic/ |
Pure, hardware-independent logic (endpoint math, SG filter/blanking, stall FSM, batch, log ring, calibration, state JSON, motor FSM). Shared by the firmware and the host tests, so tested code == shipped code. |
host_test/ |
Host unit tests — one folder per module, plus the CMake+CTest harness that builds them. |
api/ (openapi.yaml, asyncapi.yaml, schemas/) |
API contract (shared JSON Schema + REST/SSE specs). |
include/lv_conf.h |
LVGL config (found via -D LV_CONF_INCLUDE_SIMPLE, wired project-wide in the root CMakeLists.txt). |
CMakeLists.txt, partitions.csv, sdkconfig.defaults |
ESP-IDF build config. |
tools/mock_server.py |
Run the web UI on your desktop without hardware. |
tools/tests/ |
pytest suite covering cliff.toml (the release version mapping) — see Release-config tests. |
docs/ |
ARCHITECTURE.md (task model, SPI bus, motion FSM — start here), Wiring, Bill of Materials, PLAN.md (migration checklist), adr/ (architecture decision records), and the upstream v1.10.0 diff. |
Grouped by concern. All groups are also on INCLUDE_DIRS (see main/CMakeLists.txt), so
#include "motion.h" resolves from anywhere — the grouping is navigational, not a
compiler-enforced module boundary.
| Path | Purpose |
|---|---|
main/ |
app_main.cpp (entry point), config.h (pins, speed profiles, tuning constants), globals.{h,cpp} (cross-module mutable state). |
main/drivers/ |
display_touch, axs5106l_touch, tmc5160_hal, tmc5160_ctrl, stepper. |
main/motion/ |
motion.{h,cpp} — the safety-critical run / jam-detection / calibration / homing state machine. |
main/net/ |
wifi_mgr, web_server, index_html.h (the compiled-in web UI). |
main/ui/ |
ui_touch — the on-device LVGL UI. |
ESP-IDF uses host_test/ for tests that run on the build machine and test_apps/ for
on-target applications you flash to a chip (77 components use test_apps/, 9 use
host_test/ in v5.3.2). Ours are host tests, so host_test/ it is — and test_apps/
stays free should real on-target tests ever be added.
Targets ESP32-C6 with a custom partition table (partitions.csv): nvs + otadata + dual OTA
app slots (~1.9 MB each) + a coredump partition.
Requires ESP-IDF >= 6.0 — see Espressif's installation instructions.
. $HOME/esp/esp-idf/export.sh # or wherever you installed it; adjust the path
idf.py set-target esp32c6 # once, per clone
idf.py build
idf.py -p /dev/ttyACM0 flash monitor # adjust the port for your OSDependencies (LVGL, esp_lvgl_port) are pinned in main/idf_component.yml and fetched
automatically by the ESP-IDF Component Manager on first build; dependencies.lock is committed
for reproducibility. managed_components/ (the downloaded source) is gitignored.
ESP-IDF >= 6.0, enforced by
main/idf_component.yml'sidf: ">=6.0"; CI and releases pin one version, in.github/actions/setup-esp-idf/action.yml— the version this port is built and tested against. ESP32-C6 itself only needs 5.1+, and the project sat on 5.3 until the 6.0 move — but 5.x no longer compiles this source, because 6.0 typedesp_lcd's cs/dc/reset config members asgpio_num_t, stopped re-exporting FreeRTOS headers transitively, and turned on-Werror=missing-field-initializers. All three are handled in-source (seemain/drivers/display_touch.cppandmain/net/wifi_mgr.cpp). (An earlier note credited FastAccelStepper's I2S-mux driver; that library was dropped for a native RMT+PCNT stepper — see ADR 0001.)
Pure logic goes in lib/autolee_logic/ and is covered by a Unity suite in host_test/test_<module>/.
Run them (no hardware needed):
cd host_test
cmake -B build
cmake --build build -j
cd build && ctest --output-on-failureFor coverage: cmake -B build -DAUTOLEE_COVERAGE=ON, then run gcovr --root .. --filter '../lib/autolee_logic/' build from host_test/.
Keep new algorithmic logic in lib/autolee_logic/ (not inline in main/) so it can be tested on
the host and reused by the firmware.
cliff.toml decides the version a release gets when the Release workflow's version input is left
empty (see Versioning & releases), so a mistake in it ships a wrong,
immutable release — and it is otherwise only exercised by cutting one. tools/tests/ pins its
behaviour: the bump mapping RELEASING.md documents, the tag_pattern, and how the
release notes render.
pip install pytest
pytest tools/testsThese run the real git-cliff against throwaway git repos, so they need it on PATH — install
it from orhun/git-cliff (CI uses the pinned,
checksum-verified copy from .github/actions/setup-git-cliff). Without it the suite skips rather
than fails, so a contributor who does not touch cliff.toml needs nothing extra.
Change anything in cliff.toml and run this suite.
Install the official ESP-IDF extension (publisher: Espressif Systems) — it bundles the toolchain setup, build/flash/monitor commands, and a serial port picker.
sdkconfig.defaults turns on, from the first boot:
- OTA rollback (
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) — an update that doesn't self-validate reverts to the last-good app automatically. - Task + interrupt watchdog with panic (
CONFIG_ESP_TASK_WDT_*,CONFIG_ESP_INT_WDT*) — a hung loop/ISR forces a reset rather than leaving a powered stepper frozen mid-stroke. - Core dump to flash (
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH) — a crash snapshot you can pull and decode instead of a silent reboot. - Brownout detection (
CONFIG_ESP_BROWNOUT_DET).
See docs/adr/0001-build-tooling-and-platform.md for what each of these concretely buys over the
old Arduino/PlatformIO setup, and honest calibration of which are genuinely new vs. just more
configurable.
- The firmware version is derived from git — ESP-IDF populates
esp_app_desc_t.versionat build time fromgit describe --always --tags --dirty. The firmware reads it viaesp_app_get_description()->version, so the serial boot banner,/api/v1/state'sversionand/api/v1/diagnostics/info'sversionall report the same string (2.0.0at a tag, otherwise a commit hash,-dirtywhen the tree is modified). There is no version constant in the source. - Tags are bare, full three-part semver,
X.Y.Z— novprefix, and never theX.Yshorthand (e.g.2.0.0, not2.0orv2.0.0). Semver.org's own FAQ is explicit thatv1.2.3"is not a semantic version" — the tag contains a version, it isn't one. Since this project's tag string is the literal reported version (inesp_app_desc_t.version, the API JSON, and the OTA identity check), a bare tag keeps that reported string an actual semver value with nothing to strip. Consistent with this project's other repos (git-dyn-semver-gradle-plugin,reqstool-*, all bare) rather than ESP-IDF's own releases, which use avprefix but are inconsistent on digit count (v5.5, thenv5.5.1...v5.5.5) — a precedent this project already rejects on the digit count, so it isn't one to defer to on the prefix either. The web UI adds a display-onlyvprefix when rendering (main/net/index_html.h) — the underlying tag/version string never has one, and neither do release artifact filenames: they areautolee-<version>-factory.bin/autolee-<version>-ota.bin, lowercase and hyphenated, so the version in a filename is byte-identical to the tag and to what the firmware reports, with nothing to add or strip when mapping between them. - There is therefore no source file to bump when releasing. Don't tag by hand, though: releases
are cut by the Release workflow (Actions → Run workflow), which resolves the version from the
Conventional Commits since the last tag (or validates one you pass explicitly), runs the full CI
suite, pauses for approval, tags, builds, asserts the version baked
into the firmware matches the tag, publishes as a prerelease with the artifacts already
attached, re-verifies the published assets, and only then promotes it to latest. A hand-pushed
tag skips all of that. See
RELEASING.md.
- Local builds can embed a stale version — reconfigure after pulling/checking out a tag.
PROJECT_VERis computed once at CMake configure time and then cached; it is NOT recomputed on everyidf.py build. In the normal edit→build→flash loop this is invisible (the C/C++ sources you're iterating on aren't what changes the version string), but if you check out a different tag/branch, or want the version to reflect a git state that changed since the last configure, a plainidf.py buildcan silently embed the old string. Runidf.py reconfigure(or deletebuild/) first if the embedded version needs to be trusted — don't assume a rebuild alone refreshes it. This is a real difference from tools like setuptools-scm/hatch-vcs (Python) or axion-release (Gradle), which recompute from git on every invocation rather than caching at a separate "configure" step. git describefinds the nearest reachable tag by commit-graph distance, not the highest semver tag. hatch-vcs/setuptools-scm and axion-release explicitly parse all reachable tags as semver and pick the highest one, specifically to stay correct across out-of-order tagging or diverging branches (e.g. a hotfix tagged on an older line after a newer major tag already exists elsewhere). Plaingit describe --tagshas no semver awareness at all — it just walks first-parent history fromHEADand reports the most recent tag it hits. For this repo's current simple, linear, single-branch history the two approaches always agree in practice, but they are not the same guarantee. If AutoLee ever grows release branches or retroactive hotfix tags, this could produce a surprising version string — there is no ESP-IDF flag to makegit_describe()semver-aware; matching that behavior would mean replacing the built-inPROJECT_VERfallback with a custom script.
Run the same formatters/linters CI's pre-commit job uses (clang-format, ruff, basic checks)
automatically before each commit:
pip install pre-commit
pre-commit install # one-time, per clone
pre-commit run --all-files # run against everything on demandWhy ESP-IDF (and what it costs vs. the earlier PlatformIO/Arduino attempt) is recorded in
docs/adr/0001-build-tooling-and-platform.md.
- Conventional Commits for commits and PR titles. The
allowed types are in
.commitlintrc.yml; the PR title is enforced by.github/workflows/check-semantic-pr.yml, since that's what becomes the commit subject onmainunder squash-merge. Those subjects are also whatcliff.tomlturns into the release version and the release notes —fix:takes a patch,feat:a minor, any!:a major, andci:/build:/tools:are left out of the notes entirely unless breaking. An unconventional subject is silently dropped from the notes. - Work on a branch and open a PR; don't push directly to
main. - Never hardcode the firmware version anywhere — it comes from
git describeviaesp_app_get_description()->version(see "Versioning & releases" above).