Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb0d0fe
build(container): add a local CI image and runner
flavio-fernandes Sep 23, 2026
2aad45c
fix(http): keep the existing file when an upload is refused
flavio-fernandes Sep 27, 2026
f702500
fix(sim): answer an oversized body with 413, like the device
flavio-fernandes Sep 27, 2026
6609d8a
feat(render): support runtime panel heights from 8 to 16 rows
flavio-fernandes Sep 23, 2026
97e41c4
feat(render): keep eight-row content in a centred band on tall panels
flavio-fernandes Sep 23, 2026
6e81d91
feat(effects): fill tall panels in BrickBreaker, Snake and the swirls
flavio-fernandes Sep 27, 2026
4f0137d
feat(effects): make BrickBreaker, Snake and PingPong real mini-games
flavio-fernandes Sep 28, 2026
0c45639
feat(rp2040): add Galactic Unicorn builds that run the core engine
flavio-fernandes Sep 24, 2026
0a57bd9
feat(rp2040): describe build features and fixed GPIO; build UF2s in CI
flavio-fernandes Sep 24, 2026
6e9bce8
feat(persistence): store preferences in LittleFS on the Pico
flavio-fernandes Sep 24, 2026
3400688
feat(hal): drive the Galactic Unicorn panel with double-buffered PIO DMA
flavio-fernandes Sep 24, 2026
c5aaa76
feat(hal): wire Unicorn buttons and light sensor to shared services
flavio-fernandes Sep 24, 2026
68a59b3
feat(net): bring shared Wi-Fi, provisioning and mDNS to the Pico W
flavio-fernandes Sep 24, 2026
73796b1
feat(rp2040): add time, reset reason, sleep and UDP services
flavio-fernandes Sep 24, 2026
d08b417
feat(http): serve the shared API and web UI from the Pico
flavio-fernandes Sep 30, 2026
354f989
feat(rp2040): reboot a hung Pico with the hardware watchdog
flavio-fernandes Sep 27, 2026
099b544
feat(rp2040): call the buttonCallback webhook from the Pico
flavio-fernandes Sep 27, 2026
6be7769
test(rp2040): add API smoke and burst scripts
flavio-fernandes Sep 27, 2026
cfa390d
feat(mqtt): enable MQTT and Home Assistant discovery on the Pico W
flavio-fernandes Sep 27, 2026
4f274c5
feat(audio): play RTTTL through the Unicorn's I2S amplifier
flavio-fernandes Sep 27, 2026
08d86d9
feat(webui): hide the scripting, audio and update controls a board lacks
flavio-fernandes Sep 27, 2026
f2ce07b
docs: add a Galactic Unicorn guide and validate filesystem reservations
flavio-fernandes Sep 27, 2026
3b8b18d
feat(script): run Berry scripts on the Pico W and Pico 2 W
flavio-fernandes Sep 27, 2026
27227cd
docs: record the Pico 2 W hardware verification and its script figures
flavio-fernandes Oct 6, 2026
0b50eca
docs: the Pico 2 W passes MQTT and Home Assistant discovery on hardware
flavio-fernandes Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ jobs:
path: dist/usb-*.bin
if-no-files-found: error

pico-build:
name: Pico firmware build (${{ matrix.env }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env: [galactic_unicorn, galactic_unicorn_2w]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Cache PlatformIO
uses: actions/cache@v5
with:
path: |
~/.platformio
~/.cache/pip
key: pio-${{ matrix.env }}-${{ hashFiles('platformio.ini') }}
- name: Install PlatformIO
run: pip install -U platformio esptool
- name: Build firmware
run: pio run -e ${{ matrix.env }}
- uses: actions/upload-artifact@v4
with:
name: uf2-${{ matrix.env }}
path: .pio/build/${{ matrix.env }}/firmware.uf2
if-no-files-found: error

docs:
name: API docs match the firmware
runs-on: ubuntu-latest
Expand Down Expand Up @@ -153,11 +185,19 @@ jobs:
# overlap or a misaligned app offset only shows up once someone flashes
# it. Cheap to check here, expensive to discover in the field.
run: python tools/check_partitions.py
- name: Check both firmwares register the shared built-in catalog
# main.cpp and main_rp2040.cpp each wire core/BuiltinCatalog.h in by hand; one that
# drifts back to its own list ships a different set of apps and effects.
run: python tools/check_builtin_registration.py
- name: Test the UF2 filesystem reservation check
# check_partitions.py also validates each UF2 environment's LittleFS size; these
# cases prove it rejects a bad or missing reservation instead of passing everything.
run: python test/test_partition_reservations.py

release:
name: Publish firmware release
if: startsWith(github.ref, 'refs/tags/v')
needs: [native-tests, webui-tests, device-build, docs]
needs: [native-tests, webui-tests, device-build, pico-build, docs]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -174,7 +214,7 @@ jobs:
- name: Install PlatformIO
run: pip install -U platformio esptool
- name: Build every firmware image
run: pio run -e awtrix -e awtrix_s3_octal -e awtrix_s3_quad
run: pio run -e awtrix -e awtrix_s3_octal -e awtrix_s3_quad -e galactic_unicorn -e galactic_unicorn_2w
- name: Stage OTA assets
# The file a user uploads to /update. One per SoC, and one more for the
# S3's quad-PSRAM boards, whose PSRAM mode is compiled into the framework
Expand All @@ -185,6 +225,12 @@ jobs:
cp .pio/build/awtrix/firmware.bin firmware-awtrix-ng.bin
cp .pio/build/awtrix_s3_octal/firmware.bin firmware-awtrix-ng-s3-octal.bin
cp .pio/build/awtrix_s3_quad/firmware.bin firmware-awtrix-ng-s3-quad.bin
- name: Stage the Galactic Unicorn images
# A Pico has no OTA: the UF2 dropped on its BOOTSEL drive is both the first install and
# every update, so each Pico gets exactly one file.
run: |
cp .pio/build/galactic_unicorn/firmware.uf2 firmware-galactic-unicorn.uf2
cp .pio/build/galactic_unicorn_2w/firmware.uf2 firmware-galactic-unicorn-2w.uf2
- name: Stage the USB install images
# What a user flashes over USB: bootloader, partition table, app. The
# table is the only part that varies with flash size, which is why one
Expand Down Expand Up @@ -225,6 +271,8 @@ jobs:
firmware-awtrix-ng.bin
firmware-awtrix-ng-s3-octal.bin
firmware-awtrix-ng-s3-quad.bin
firmware-galactic-unicorn.uf2
firmware-galactic-unicorn-2w.uf2
usb-awtrix-ng.zip
licenses-awtrix-ng.zip
- name: Flip the release live, now that every asset is attached
Expand Down
17 changes: 16 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ pip install -U platformio
Node 22+ is needed for a firmware build (the web UI is minified through `npx`
before it is embedded) and for the web UI tests.

## The four build targets
## The build targets

```bash
pio run -e awtrix # ESP32 firmware (stock pin defaults)
pio run -e awtrix_s3_octal # ESP32-S3 firmware, octal PSRAM
pio run -e awtrix_s3_quad # ESP32-S3 firmware, quad PSRAM
pio run -e galactic_unicorn # Galactic Unicorn firmware, Pico W
pio run -e galactic_unicorn_2w # Galactic Unicorn firmware, Pico 2 W
python scripts/test_native.py # host unit tests for the portable core
pio run -e native_sim # host simulator: full firmware + web UI, no hardware
```
Expand All @@ -63,16 +65,29 @@ Every push runs, and your PR needs all of it green:
python scripts/test_native.py # host unit tests
pio run -e awtrix # both firmware images build
pio run -e awtrix_s3_octal
pio run -e galactic_unicorn # both Pico UF2s build
pio run -e galactic_unicorn_2w
python tools/check_docs_sync.py # docs match the firmware's real fields
python tools/check_berry_api.py # editor's Berry API table is current
python tools/gen_agent_skill.py --check
python tools/check_prelude_solidified.py
python tools/check_font_sync.py
python tools/check_partitions.py
python tools/check_builtin_registration.py
python test/test_partition_reservations.py
mkdocs build --strict # docs build, no broken links or anchors
cd webui/test && npm install && npm test
```

To run them all without installing a toolchain, use the CI image in
`tools/container/`:

```bash
podman build -t awtrix-build tools/container
podman run --rm -v "$PWD":/w:Z -v pio-home:/root/.platformio \
awtrix-build tools/container/ci-local.sh all
```

### Generated files you may have to regenerate

Several checked-in files are produced by generators that **no normal build
Expand Down
21 changes: 21 additions & 0 deletions LICENSES/MIT-Pimoroni.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Pimoroni Ltd

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ or skip the middleman entirely and **run your app on the device itself.**

[![CI](https://github.com/Blueforcer/awtrix-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/Blueforcer/awtrix-ng/actions/workflows/ci.yml)
[![Docs](https://github.com/Blueforcer/awtrix-ng/actions/workflows/docs.yml/badge.svg)](https://github.com/Blueforcer/awtrix-ng/actions/workflows/docs.yml)
![Platform](https://img.shields.io/badge/platform-ESP32%20%7C%20ESP32--S3-blue)
![Platform](https://img.shields.io/badge/platform-ESP32%20%7C%20ESP32--S3%20%7C%20RP2040%20%7C%20RP2350-blue)
[![License](https://img.shields.io/badge/license-PolyForm%20Noncommercial%201.0.0-lightgrey)](LICENSE.md)

📖 **[Documentation](https://blueforcer.github.io/awtrix-ng/)**  · 
Expand Down Expand Up @@ -100,8 +100,11 @@ animations in the browser, and you can share anything you make back to the galle

## 🧩 Hardware & source

Any 32(-128)×8 WS2812-style panel: a commercial clock, an AWTRIX 2 conversion or your own build - same
image, pins set in the web UI. ESP32 and ESP32-S3.
Any 32–128 × 8–16 WS2812-style panel: a commercial clock, an AWTRIX 2 conversion or your own build - same
image, pins set in the web UI. ESP32 and ESP32-S3. Also supports the fixed 53×11
[Pimoroni Galactic Unicorn](docs/advanced/galactic-unicorn.md) with a Pico W
(RP2040) or Pico 2 W (RP2350), both hardware-verified. Pico images use USB UF2
updates; optional features follow the device's capabilities.

```bash
pio run -e awtrix # build + flash
Expand Down
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
**Added**

- **Pimoroni Galactic Unicorn support.** Dedicated Pico W and Pico 2 W UF2 builds, fixed 53×11 PIO/DMA display, light sensor, buttons, I²S RTTTL melodies, LittleFS persistence, Wi-Fi provisioning, HTTP/MQTT API, Home Assistant, NTP, discovery, Art-Net and watchdog recovery. Both Pico W and Pico 2 W are hardware-verified.
- **Tall panels.** Configurable height 8–16 for generic panels, with eight-row content centred above full-canvas effects, backgrounds, charts and transitions.
- **Capability-driven controls.** Builds without scripting hide its toggle and editor; unsupported MP3/radio controls remain hidden with an availability note. The Pico builds omit MP3/radio, outbound TLS and browser OTA, so the web UI offers no browser install there; use USB BOOTSEL updates and note that timed sleep is emulated, not deep sleep.
- **Berry scripting on the Galactic Unicorn.** Both Pico builds run scripts and report `scripting: true`: a 48 KB script heap on the Pico W, about 7 KB per script at most, and 96 KB on the Pico 2 W, about 28 KB per script. Script HTTP requests, Modbus reads and script icons are not available on the Pico.
- **Install firmware updates from the web UI.** Click **Check for updates**, then **Download & install** and confirm. Updates are checked only when you ask. Your browser downloads and verifies the correct image, then uploads it to AWTRIX with progress shown throughout. Settings and files are kept.
- **Modbus TCP for Berry scripts.** Read holding registers, input registers, coils and discrete inputs from local devices without blocking the display. Helpers decode signed integers and 32-bit floating-point values, with configurable host, port and unit ID.
- **Timers and delayed actions.** `timer.after()` runs a callback once; `timer.every()` repeats it; `timer.cancel()` cancels it. Timers work in the background, including in headless scripts, and are cleaned up when a script is replaced, removed or fails.
Expand All @@ -9,6 +13,7 @@

**Changed**

- **BrickBreaker, Snake and PingPong are real mini-games on every panel size.** BrickBreaker shows separate 3-pixel bricks (a third row from 11 rows up) knocked out one at a time, with two bounce angles. Snake chases red food and grows. PingPong has a paddle on each side. BrickBreaker and PingPong keep fixed colours; Snake still takes the palette for its body.
- **GIFs keep their own size, up to the panel dimensions.** Scripts, pushed apps and notifications support GIFs sized for custom panels. Text layout follows the icon's actual width; a panel-wide GIF becomes a background.
- Each script app manages its own icons and releases them when hidden. File access and streamed log and Wi-Fi scan responses use less temporary memory.
- The scripting documentation, AI prompt and downloadable agent skill cover Modbus, timers and extended button events, with corrected settings instructions and guidance for different panel sizes and PSRAM budgets.
Expand All @@ -27,3 +32,5 @@
| Classic ESP32 - Ulanzi TC001, AWTRIX 2 conversions, most DIY | `firmware-awtrix-ng.bin` | `usb-awtrix-ng-<flash>.bin` |
| ESP32-S3, octal PSRAM (`N8R8`, `N16R8`) or no PSRAM | `firmware-awtrix-ng-s3-octal.bin` | `usb-awtrix-ng-s3-octal-<flash>.bin` |
| ESP32-S3, quad PSRAM (`N8R2`, `N16R2`, `N4R2`) | `firmware-awtrix-ng-s3-quad.bin` | `usb-awtrix-ng-s3-quad-<flash>.bin` |
| Galactic Unicorn, Pico W | USB BOOTSEL only | `firmware-galactic-unicorn.uf2` |
| Galactic Unicorn, Pico 2 W | USB BOOTSEL only | `firmware-galactic-unicorn-2w.uf2` |
10 changes: 10 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ ones ask for.

---

## Galactic Unicorn display (Pico firmware)

The display initialization, 14-plane stream layout and PIO program are adapted
from [pimoroni/pimoroni-pico](https://github.com/pimoroni/pimoroni-pico/tree/v1.23.0/libraries/galactic_unicorn),
tag **v1.23.0**, commit `3440ab232cdc2b019bb8d16f67b0448502efd9dc`.
Copyright (c) 2021 Pimoroni Ltd; **MIT**, full notice in
[MIT-Pimoroni.txt](LICENSES/MIT-Pimoroni.txt).
The port excludes pico_graphics and audio, adds dynamic PIO allocation and
tear-free double buffering, and uses the NG gamma setting at 14-bit precision.

## Compiled into the firmware image

These are linked into `firmware-awtrix-ng*.bin`, so their notices travel with
Expand Down
48 changes: 37 additions & 11 deletions docs/advanced/building.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Building from source

AWTRIX NG is a [PlatformIO](https://platformio.org/) project. Five environments in
`platformio.ini` cover the device firmware, the host unit tests and the host
simulator.
AWTRIX NG is a [PlatformIO](https://platformio.org/) project. Its environments in
`platformio.ini` cover the ESP32 and Galactic Unicorn firmware, the host unit tests
and the host simulator.

You need PlatformIO, Python 3 and Node.js. The host tests additionally need CMake
3.20+, Ninja and GCC. Node is used only by the pre-build step that embeds the
Expand All @@ -12,6 +12,7 @@ You need PlatformIO, Python 3 and Node.js. The host tests additionally need CMak
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
pio run -e galactic_unicorn # Galactic Unicorn firmware, Pico W (RP2040)
python scripts/test_native.py # host unit tests for the portable core
pio run -e native_sim # host simulator: firmware + web UI without an ESP32
```
Expand All @@ -21,8 +22,10 @@ builds the ESP32 firmware.

## Build environments

There are **six**. Three build device firmware (`awtrix`, `awtrix_s3_octal`,
`awtrix_s3_quad`), one is a measurement build of the device firmware (`awtrix_probe`),
There are **nine**. Five build device firmware (`awtrix`, `awtrix_s3_octal`,
`awtrix_s3_quad`, `galactic_unicorn`, `galactic_unicorn_2w`), two are
measurement builds of the device firmware (`awtrix_probe`,
`galactic_unicorn_udp_measure`),
and two run on the host (`native`, `native_sim`).

### `awtrix` - the ESP32 firmware
Expand All @@ -41,7 +44,7 @@ is runtime configuration, not a compile-time choice - see
| Partitions | generated by `scripts/gen_partitions.py` |
| Upload speed | 921600 |
| Monitor speed | 115200 (with `esp32_exception_decoder`) |
| Source filter | `+<*> -<sim/> -<core/script/ScriptHeapNative.cpp>` |
| Source filter | `+<*> -<sim/> -<platform/rp2040/> -<hal/GalacticUnicornBoard.cpp> -<core/script/ScriptHeapNative.cpp>` |

```bash
pio run -e awtrix # build
Expand Down Expand Up @@ -111,6 +114,28 @@ built for the other PSRAM type with `wrongChip` rather than installing it.
pio run -e awtrix_s3_quad
```

### `galactic_unicorn` and `galactic_unicorn_2w` - the Galactic Unicorn firmware

Pimoroni's Galactic Unicorn, a fixed 53×11 panel, on a Pico W (`galactic_unicorn`) or a
Pico 2 W (`galactic_unicorn_2w`). The source filter is an explicit allowlist, so only
the portable core and the services ported to the Pico are linked.
See [Galactic Unicorn](galactic-unicorn.md) for flashing and features.

| Property | Value |
|---|---|
| Platform | `maxgerhardt/platform-raspberrypi`, pinned to a tested commit |
| Board | `rpipicow` / `rpipico2w` |
| Framework | `arduino`, earlephilhower core |
| Filesystem | 512 KiB LittleFS |
| Output | `.pio/build/<env>/firmware.uf2`, flashed over USB BOOTSEL |

```bash
pio run -e galactic_unicorn
```

`galactic_unicorn_udp_measure` is `galactic_unicorn` without the discovery and Art-Net
services. It only exists to measure what they cost; CI does not build it.

### `awtrix_probe` - the heap measurement build

`awtrix` plus `-D AWTRIX_HEAP_PROBE` and link-time wrapping of `malloc`, `free`,
Expand All @@ -133,7 +158,7 @@ provides the declared native dependencies.
|---|---|
| Platform | `native` (host compiler) |
| Test framework | `unity` |
| Source filter | `+<core/>` plus `transport/ScriptMqttBridge.cpp` |
| Source filter | `+<core/>` plus `platform/SocProfile.cpp` and `transport/ScriptMqttBridge.cpp` |
| Extra `lib_deps` | `berry` (the vendored interpreter) and `base64` |
| Optimisation | `-O2` |

Expand Down Expand Up @@ -161,7 +186,7 @@ stand-in board hardware, a small HTTP server, and the web UI served from disk. S
|---|---|
| Platform | `native` (host compiler) |
| Defines | `AWTRIX_NATIVE`, `AWTRIX_SIM` |
| Source filter | `core/`, `sim/`, plus `media/GifPlayer.cpp`, `media/MicroGif.cpp`, `media/ScriptIcon.cpp`, `persistence/DeviceConfigJson.cpp`, `persistence/SystemConfigApply.cpp`, `persistence/FsRestoreSink.cpp`, `system/Log.cpp`, `transport/ScriptMqttBridge.cpp`, `transport/DeviceStateJson.cpp`, `transport/mqtt/MqttService.cpp`, `transport/mqtt/MqttLink.cpp`, `transport/mqtt/HaAnnouncer.cpp` |
| Source filter | `core/`, `sim/`, plus `platform/SocProfile.cpp`, `media/GifPlayer.cpp`, `media/MicroGif.cpp`, `media/ScriptIcon.cpp`, `persistence/DeviceConfigJson.cpp`, `persistence/SystemConfigApply.cpp`, `persistence/FsRestoreSink.cpp`, `system/Log.cpp`, `transport/ScriptMqttBridge.cpp`, `transport/DeviceStateJson.cpp`, `transport/mqtt/MqttService.cpp`, `transport/mqtt/MqttLink.cpp`, `transport/mqtt/HaAnnouncer.cpp` |
| Extra `lib_deps` | `base64`, `PubSubClient` |

```bash
Expand Down Expand Up @@ -279,10 +304,11 @@ for the firmware builds and the web UI tests:
| Core host unit tests | `python scripts/test_native.py` | The portable `core/` layer |
| Web UI tests (jsdom) | `npm test` (in `webui/test`) | The web UI JS logic, loaded from the shipped `webui/index.html` via jsdom |
| Firmware build | `pio run -e <env>`, then `scripts/factory_image.py --all` | Every device image - the matrix is `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`, `tools/check_berry_api.py`, `tools/check_prelude_solidified.py`, `tools/check_font_sync.py`, `tools/check_partitions.py` | Documented fields and error codes, the editor's Berry table, the solidified prelude, the generated panel font, and every partition table |
| Pico firmware build | `pio run -e <env>` | `galactic_unicorn` and `galactic_unicorn_2w`; each UF2 is uploaded as the `uf2-<env>` artifact |
| API docs match the firmware | `tools/check_docs_sync.py`, `tools/check_berry_api.py`, `tools/check_prelude_solidified.py`, `tools/check_font_sync.py`, `tools/check_partitions.py`, `tools/check_builtin_registration.py`, `test/test_partition_reservations.py` | Documented fields and error codes, the editor's Berry table, the solidified prelude, the generated panel font, every partition table, and the shared built-in catalog; the partition check also validates each UF2 filesystem reservation |

On a `v*` tag a release job additionally publishes every OTA image and the USB install
images for each supported flash size.
On a `v*` tag a release job additionally publishes every OTA image, the USB install
images for each supported flash size and the Galactic Unicorn UF2s.

A second workflow, `.github/workflows/docs.yml`, builds this documentation with
`mkdocs build --strict` and fails on broken links and anchors.
Loading