How to build, test and change the firmware. The components and the session flow are described in architecture.md.
The firmware is an ESP-IDF 5.5 project (CI and the dev container use
espressif/idf:v5.5.6). For the main board:
idf.py set-target esp32s3
idf.py menuconfig # optional, see below
idf.py build flash monitorOther boards add their defaults file from boards.json in
a build directory of their own, for example the T-Dongle-C5:
idf.py -B build-tdongle-c5 -D IDF_TARGET=esp32c5 \
-D SDKCONFIG=build-tdongle-c5/sdkconfig \
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.tdongle-c5" \
buildThe board and the preset are in main/Kconfig.projbuild (menu
TrailCamHub). Every other option is in the Kconfig file of the
component that uses it, in menuconfig under Component config →
TrailCamHub: … (for example components/session_controller/Kconfig,
TrailCamHub: session). An option used by several components is with the
one that owns the function. modes.md lists
which settings are build options and which can be changed on the hub.
Useful ones while investigating a camera:
- Wi-Fi enable payload (hex) —
BT_KEY_ONby default; spaces, colons and hyphens may separate the bytes; empty = only inspect GATT profiles. The hub writes with response if the characteristic supports it. - Log camera UDP packet contents as hex — every camera packet in 16-byte lines below its summary.
- Memory log interval — free and minimum heap (internal and PSRAM) every 30 s; 0 logs only at fixed stages.
At boot the firmware logs the reset reason, PSRAM, whether a core dump is
stored (read it with idf.py coredump-info) and mounts the SD card at
/sdcard without ever formatting it; a missing card is not fatal.
Every cycle has a number that goes on across deep sleep. The log lines of a
cycle on the SD card start with it (#12 …), the console's status shows
the last cycle with its lowest free heap and stack, the check-in sends these
figures (memory), and /sdcard/logs/cycles.jsonl keeps one line per cycle
(sd-layout.md).
- Development (default): only
sdkconfig.defaults(+ the board file). Debug-friendly optimisation and INFO logs including the camera packets. - Release: adds
sdkconfig.defaults.release— optimised for size, assertion messages silenced, INFO/DEBUG and Bluetooth stack logs removed at compile time. Use it to judge size and RAM headroom.
idf.py -B build-release -D IDF_TARGET=esp32s3 \
-D SDKCONFIG=build-release/sdkconfig \
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" \
build sizeThe VS Code task ESP-IDF: Build release firmware and show size runs the
same. Settings for all profiles belong in sdkconfig.defaults; size settings
that hinder debugging in sdkconfig.defaults.release.
Parsers, packet builders, state machines and schedules have no ESP-IDF dependency and are tested on the development machine with doctest, including packets captured from a TC100. No board and no ESP-IDF are needed:
cmake -S test/host -B build-host -DCMAKE_BUILD_TYPE=Debug
cmake --build build-host --parallel
ctest --test-dir build-host --output-on-failureCI runs them a second time with AddressSanitizer and
UndefinedBehaviorSanitizer (-DHOST_TESTS_SANITIZE=ON in a build directory
of its own); a finding fails the run.
Files covered by host tests must not include ESP-IDF headers. New parsing or
protocol logic belongs in such files, with a test in test/host.
For bench tests without the real camera, an ESP32-WROOM can play the TC100:
tools/camera_mock/ is a separate ESP-IDF
project that advertises like the camera, opens its access point and answers
discovery, login, device info and the media list with responses replayed
from real captures. A hub runs a full camlist cycle against it.
The Dev Container configuration uses
the same espressif/idf:v5.5.6 image as CI
(Dockerfile), mounts the repository at
/project and adds the tools of the other CI checks: clang-format 23.1.3,
Node 22 for app/ and backend/, and the GitHub CLI. Every shell has
ESP-IDF set up, ccache is on, and /root/.cache is a volume that survives
rebuilds. To flash and monitor boards on USB from the container, use
.devcontainer/hardware/.
Keep the /project mount point: CMake records absolute paths in
build/CMakeCache.txt. To apply a changed configuration to a Codespace, run
Codespaces: Rebuild Container (or Dev Containers: Rebuild Container);
this keeps build/. Check the environment with:
printf 'IDF_PATH=%s\n' "$IDF_PATH" # /opt/esp/idf
idf.py --version # 5.5.6Build with the default VS Code task ESP-IDF: Build firmware
(Ctrl+Shift+B) or idf.py build; on a fresh checkout run
idf.py set-target esp32s3 once. build/ and the generated sdkconfig are
ignored by Git; keep build/ for incremental builds instead of running
idf.py fullclean routinely.
clang-tidy checks the files that build without ESP-IDF (the sources of the
host tests) with the checks in .clang-tidy; every finding fails CI:
.github/scripts/static-analysis.sh # uses or creates build-hostFix a finding, or suppress it with a NOLINT(<check>) comment that says why.
CI uses the clang-tidy of Ubuntu 24.04 (18).
| Workflow | What it does |
|---|---|
| Firmware Package | builds every board (development and release), the camera mock, runs the host tests (also with ASan/UBSan), clang-tidy on the files of the host tests and the clang-format check, reports size per component (on PRs compared with the target branch, warning below 10 % free) and the static RAM of every board (see below), publishes the installer from main and releases from tags |
| App | lints, tests and builds the Telegram Mini App |
| Backend | tests and deploys the optional AWS backend |
| Badges | updates the README badges |
Before pushing: format with clang-format 23
(git ls-files '*.c' '*.cpp' '*.h' '*.hpp' | xargs clang-format -i) and run
the host tests. The ESP-IDF build only compiles; start-up and runtime paths
need a review or a test on hardware.
The summary of every board build has a Static RAM table
(.github/scripts/ram-report.sh): used and free bytes of the RAM regions
(DRAM and IRAM on the ESP32; one shared region on the newer chips, called
DIRAM or HP SRAM) for the development and the release image. What is free of
the data RAM is the heap at boot. The job warns below 10 % free IRAM on the
ESP32 (the link fails when it is full, #209) and below 25 % free data RAM.