Building from source¶
AWTRIX NG builds with two systems. PlatformIO builds the ESP32
firmware and runs the unit tests of the portable core. CMake builds awtrix-linux, the
application the Ulanzi TC002 runs, together with every host contract test. The PlatformIO
configuration is
platformio.ini; the CMake
entry points are
CMakeLists.txt and
CMakePresets.json.
You need PlatformIO, Python 3 and Node.js. The host tests also need CMake 3.20 or newer, Ninja and GCC. Node.js is used only by the step that embeds the web UI, but a firmware build stops without it.
pio run -e awtrix # ESP32 firmware (the default environment)
pio run -e awtrix_s3_octal # ESP32-S3 firmware, octal PSRAM
pio run -e awtrix_s3_quad # ESP32-S3 firmware, quad PSRAM
python scripts/test_native.py # host unit tests for the portable core
awtrix is the default environment (default_envs = awtrix), so a bare pio run builds the
ESP32 firmware.
Build environments¶
platformio.ini has six environments. Three build device firmware (awtrix,
awtrix_s3_octal, awtrix_s3_quad). Two are measurement builds of it (awtrix_probe,
awtrix_s3_octal_probe). One runs the host unit tests (native).
awtrix - the ESP32 firmware¶
The default build, and the image for every classic ESP32 board: commercial 32×8 clocks, AWTRIX 2 mainboard conversions and DIY builds all flash the same file. The GPIO map is runtime configuration, not a compile-time choice. See GPIO & boards.
| Property | Value |
|---|---|
| Platform | espressif32@6.12.0 |
| Board | esp32dev |
| Framework | arduino |
| CPU | 240 MHz (board_build.f_cpu = 240000000L) |
| Partitions | generated by scripts/gen_partitions.py |
| Upload speed | 921600 |
| Monitor speed | 115200 (with esp32_exception_decoder) |
| Source filter | +<*> -<platform/> +<platform/esp32/>, without core/synth/ and core/audio/PitchDetector.cpp |
pio run -e awtrix # build
pio run -e awtrix -t upload # build + flash over USB
pio run -e awtrix -t upload -t monitor # flash + open the serial monitor
The build product is .pio/build/awtrix/firmware.bin. It is the same file you would upload
through the web UI's update page.
awtrix_s3_octal - the ESP32-S3 firmware¶
The same sources built for an ESP32-S3 with octal PSRAM (external pseudo-static RAM), for generic
DIY builds. It differs from awtrix by one build flag, AWTRIX_SOC_ESP32S3. That flag selects the
S3 pin rules, the compiled matrix drivers and the pin defaults. Everything else is identical.
| Property | Value |
|---|---|
| Board | esp32-s3-devkitc-1-n16r8 (in boards/) |
| Flash | 16 MB, quad (flash_mode = qio) |
| PSRAM | octal, memory_type = qio_opi |
| Serial | UART0, the port the DevKitC-1's USB bridge is wired to |
A board file describes one module. N16R8 is the ESP32-S3-WROOM-1 with 16 MB quad flash and
8 MB octal PSRAM. An S3 without PSRAM runs this image as it is: the octal framework libraries
carry CONFIG_SPIRAM_IGNORE_NOTFOUND, so a failed PSRAM init is logged and the boot continues. A
board with quad PSRAM needs awtrix_s3_quad.
The V suffix some sellers add (N16R8V, N32R8V) is a different module: WROOM-2, 1.8 V, with
octal flash. It needs memory_type = opi_opi and a matching bootloader, so it would need its
own board file and image. Neither ships.
R8 says 8 MB, not which bus. On Espressif's own modules R8 is octal. Boards that carry
their own PSRAM chip beside the SoC differ: an ESP-PSRAM64H is 8 MB over quad. Such a board
needs awtrix_s3_quad despite its N16R8 label, and the released
usb-awtrix-ng-s3-quad-16mb.bin matches its flash. For a local build, the 8 MB board file works
as it is; its partition table leaves the upper half of the flash unused.
awtrix_s3_quad - the S3 firmware for quad PSRAM¶
Same sources and same flags as awtrix_s3_octal. Only the board file differs:
| Property | Value |
|---|---|
| Board | esp32-s3-devkitc-1-n8r2 (in boards/) |
| Flash | 8 MB, the common quad module. The released USB images carry a table per flash size regardless |
| PSRAM | quad, memory_type = qio_qspi |
memory_type picks which set of precompiled framework libraries the app links against. Those
carry CONFIG_SPIRAM_MODE_OCT or CONFIG_SPIRAM_MODE_QUAD. The mode is compiled in, never
detected, so one image cannot serve both kinds of board.
A wrong choice has different results in each direction:
| Image | On an octal board | On a quad board | With no PSRAM |
|---|---|---|---|
awtrix_s3_octal |
correct | boots, PSRAM invisible: no radio, Berry heap on internal RAM | boots, same as a quad board |
awtrix_s3_quad |
boot loop | correct | boot loop |
The boot loop comes from the quad libraries: they lack CONFIG_SPIRAM_IGNORE_NOTFOUND, so a
failed PSRAM init calls abort(). Only a USB flash undoes it. That is why POST /update refuses
an image built for the other PSRAM type with wrongChip instead of installing it.
awtrix_probe - the heap measurement build¶
awtrix plus -D AWTRIX_HEAP_PROBE and link-time wrapping of malloc, free, calloc and
realloc. Every allocation the loop task makes is counted and reported through the ordinary
log. Optimisation stays at the release -Os. It is never released and CI does not build it.
awtrix_s3_octal_probe is the same for the S3.
native - host unit tests¶
The core/ layer is portable C++17 without Arduino or FastLED, so it compiles and runs on your
development machine. The fast runner uses CMake, Ninja and CTest with the Unity test framework.
No ESP32 and no emulator are needed. PlatformIO still provides the declared dependencies.
| Property | Value |
|---|---|
| Platform | native (host compiler) |
| Test framework | unity |
| Sources | test/CMakeLists.txt: core/ (via cmake/Core.cmake, with platform/linux/host/HostScriptHeap.cpp) plus transport/ScriptMqttBridge.cpp; PlatformIO only installs the dependencies (build_src_filter = -<*>) |
Extra lib_deps |
berry (the vendored interpreter), base64 and Unity |
| Optimisation | -O2 |
python scripts/test_native.py # build and run every host suite
python scripts/test_native.py -R payload # run matching suites after the build
python scripts/test_native.py -j 8 # limit parallel build and test jobs
The runner compiles the portable core once, links the suite executables in parallel and runs them
in parallel with CTest. The suites live in
test/. Generated files go to
.pio/native-tests/. Both this runner and the Linux build use cmake/Core.cmake for the engine.
The PlatformIO native environment supplies dependencies for the CMake tests.
On Windows, test/CMakeLists.txt links the GCC runtimes statically into the host
binaries. See Host toolchain.
awtrix-linux with CMake¶
awtrix-linux is the application the Ulanzi TC002 runs. Without --board tc002 it is a headless
Linux development target with a fixed display, real MQTT and persistent state. CMake builds it
together with every host contract test. Run this on Linux, or in WSL (Windows Subsystem for Linux),
from the repository root. Debian and Ubuntu packages are shown; mosquitto is only needed by the
MQTT tests:
sudo apt-get install -y build-essential cmake libssl-dev liblzma-dev libjpeg-turbo8-dev openssl mosquitto
pio pkg install -e native
cmake --preset host
cmake --build --preset host --parallel
ctest --preset host --parallel 8
build/host/awtrix-linux --data "$HOME/.local/share/awtrix-ng-dev" --port 8080 \
--webui "$PWD/webui/index.html"
Then open http://127.0.0.1:8080. It listens on loopback only. The flags and the run modes are
described in AWTRIX on Linux. The TC002 release, built for ARM with the tc002
and tc002-loader presets, is described in the TC002 developer guide.
| Preset | Toolchain | Builds |
|---|---|---|
host |
the machine's compiler | awtrix-linux and every regression test |
tc002 |
cmake/toolchains/tc002-musl.cmake |
the static ARM programs of a TC002 release |
tc002-loader |
cmake/toolchains/tc002-glibc.cmake |
libawtrix-loader.so |
A compiler warning in AWTRIX code stops the build. With a newer compiler that warns about more,
configure with -DAWTRIX_WERROR=OFF.
The embedded web UI¶
The tab sources live in webui/src/; manifest.json lists their assembly order.
python scripts/webui_source.py writes the single webui/index.html served by
Linux. PlatformIO, CMake and the jsdom harness run the same assembler. Use
--check to detect an outdated generated file without rewriting it.
Before compiling, the pre:scripts/build_webui.py extra script assembles, minifies and gzips
the HTML into src/transport/http/WebUiAsset.h. That header is generated and checked
in. The compressed asset has a 92 KB budget; the build fails when it is exceeded. The header is
rewritten only when webui/index.html has changed, so incremental builds stay incremental.
Minification runs html-minifier-terser, pinned to one exact version, through npx. Node.js
has to be on PATH whenever the web UI source has changed since the header was generated.
Without npx the build stops rather than embedding an unminified asset.
Commit source edits and generated HTML together. Merge the sources and rebuild;
never merge WebUiAsset.h by hand. Commit a changed header separately. The TC002
bundler checks that the HTML and embedded asset match their inputs.
Partition tables are generated¶
There is no checked-in partition table. pre:scripts/gen_partitions.py derives one from the
board's SoC and flash size and points the build at it. The app slot is sized from the measured
firmware plus at least a 20 % growth margin, not from the available flash. SPIFFS takes the
rest, minus a 64 KB coredump region. One slot size covers every SoC and every flash variant.
| Flash | App slot (two of them) | SPIFFS |
|---|---|---|
| 4 MB | 1.69 MB | 512 KB |
| 8 MB | 1.69 MB | 4.5 MB |
| 16 MB | 1.69 MB | 12.5 MB |
python tools/check_partitions.py renders every combination and checks the rules that make a
table bootable. It runs in CI.
To inspect or produce one by hand:
USB install images¶
pio run -t upload writes the bootloader, the partition table, boot_app0 and the app as
separate transfers. A USB install image merges them into one file starting at offset 0, so it can
be flashed with nothing but esptool. The usb-*.bin release assets are these images.
Flashing is the user page for them.
pio run -e awtrix
python scripts/factory_image.py --env awtrix --flash-size 4MB -o dist/usb-awtrix-ng-4mb.bin
Merging runs through esptool, which is not a PlatformIO dependency: run pip install esptool
first.
--all instead of --flash-size builds every variant that SoC ships in (4/8/16 MB for the
ESP32, 8/16 MB for the S3) into a directory, named like the release assets. The flash size selects
the partition table; the app image inside is the same across all of them.
The merged file places the pieces at 0x1000 (bootloader; 0x0 on the S3), 0x8000 (partition
table), 0xE000 (boot_app0.bin) and 0x10000 (the app), with 0xFF padding between. 0xFF is
what erased flash reads as, so writing the image also clears NVS (the settings store). The file
ends where the app ends, so it is about 1.5 MB rather than a full flash image. The SPIFFS region
is left untouched.
An update over the network never rewrites the partition table. A changed layout only takes effect after a full USB flash of one of these images.
Vendored libraries¶
Three libraries are vendored: their source lives in the repository under lib/ instead of
coming from the PlatformIO registry.
lib/ directory |
What it is |
|---|---|
berry |
The Berry scripting language runtime |
PubSubClient |
The PubSubClient MQTT client, 2.8 with checked bounds on inbound packets |
TJpg_Decoder |
The TJpg_Decoder JPEG decoder |
PlatformIO's Library Dependency Finder links PubSubClient and TJpg_Decoder into the device
build without a lib_deps entry. berry is named in the native environment's lib_deps, which
resolves to the local copy. The CMake host build compiles lib/PubSubClient and lib/berry directly. Every other
device dependency (FastLED, base64, the Adafruit sensor libraries) is a registry dependency listed
under lib_deps.
Host toolchain¶
The native test runner compiles with your host GCC. On Windows,
scripts/test_native.py checks whether gcc and g++ are on PATH. If they are not, it falls
back to a portable w64devkit at D:\tools\w64devkit
(override with the W64DEVKIT environment variable). Nothing is installed system-wide: extract a
w64devkit release and you are done. On
Linux and macOS the system compiler is used directly.
Continuous integration¶
.github/workflows/ci.yml runs on every push to any branch, on v* tags, on pull requests
targeting main, and on manual dispatch. Its jobs run on ubuntu-latest with Python 3.12 and
PlatformIO installed through pip, plus Node.js for the firmware builds and the web UI tests:
| Job | Command | Covers |
|---|---|---|
| Core host unit tests | python scripts/test_native.py |
The portable core/ layer |
| Linux application and shared host contracts | CMake build, CTest, tests/linux/test_service.py |
The Linux executable, HTTP/MQTT/script/persistence contracts, HTTPS certificate checks, the systemd service, TC002 tool tests without hardware |
| Web UI tests (jsdom) | npm test in webui/test, then node --test for the flow converter |
The web UI logic loaded from the shipped webui/index.html, tools/flowconv |
| TC002 image tools | Reusable tc002-image-tools.yml |
The pinned WASM tools and browser/desktop installer tests; CI, Docs and installer packaging reuse the verified cache for identical inputs |
| Firmware build | pio run -e <env>, then scripts/factory_image.py --all |
awtrix, awtrix_s3_octal, awtrix_s3_quad, and a USB install image per flash size |
| API docs match the firmware | tools/check_docs_sync.py, check_flow_converter.py, gen_agent_skill.py --check, check_berry_api.py, check_prelude_solidified.py, check_font_sync.py, check_partitions.py |
Documented fields and error codes, the flow converter's maps, the downloadable agent skill, the editor's Berry table, the solidified prelude, the generated panel fonts, every partition table |
On a v* tag a release job also publishes every ESP32 update image, the USB install images for
each supported flash size, the TC002 update package, the TC002 USB installation package with its
corresponding sources, the TC002 desktop installers and the license notices. The tag must match
version, and docs/releases/<version>.md must exist: the release body only links that page in
each clock's documentation. The job depends on the native, web UI, TC002 image tools, TC002 update
package, TC002 installer, firmware and documentation checks.
Other workflows:
| Workflow | Runs on | Does |
|---|---|---|
docs.yml |
pushes and pull requests to main, and after a release |
Builds this site with tools/docs/build_site.py: one documentation per clock (fails on broken links and anchors, and on text that names another clock), including the browser flasher and the TC002 browser installer, and publishes it to GitHub Pages |
tc002-update.yml |
pushes to main and v* tags (called by ci.yml), and on dispatch |
Builds awtrix-ng-tc002.awup from scratch: the musl toolchain (cached until the Buildroot version, configuration or patches change), the glibc toolchain, the kernel tree, build_bundle.sh and tc002_install.py package; then tc002-sources.tar.gz (source_package.py) and usb-awtrix-ng-tc002.zip (browser_package.py) |
tc002-installer.yml |
v* tags (called by ci.yml), and pushes that change tools/tc002/desktop/, tools/tc002/browser-image/, docs/assets/tc002/ |
Builds and tests the TC002 desktop installer for Windows, macOS and Linux |
system-build.yml |
pull requests that change firmware/buildroot/, tools/system/ |
Cross-builds the experimental ARM userspace |
discord-release.yml |
a published release, or dispatch by the release job | Announces a release |
Fonts¶
The panel fonts are generated C++ headers. python scripts/gen_matrix_fonts.py regenerates
src/media/MatrixFonts.h and src/media/MatrixFontsCompact.h; python tools/check_font_sync.py
checks them and the legacy fonts against their sources. The ESP32 build sets
AWTRIX_COMPACT_FONTS=1 and carries four fonts; Linux and the TC002 carry all twelve. The native
tests exercise both catalogs. Sources, generators and licensing are in Fonts.