Skip to content

feat: Pimoroni Galactic Unicorn (Pico W / Pico 2 W) support and tall panels - #1

Draft
flavio-fernandes wants to merge 28 commits into
mainfrom
feat/galactic-unicorn
Draft

flavio-fernandes wants to merge 28 commits into
mainfrom
feat/galactic-unicorn

Conversation

@flavio-fernandes

@flavio-fernandes flavio-fernandes commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Status 2026-10-07: the upstream copy, Blueforcer#69, was closed by the maintainer without merging. Work continues here. This branch stays on its 1.2.1 base (snapshot galactic-unicorn.rc11 = 0b50eca); porting onto upstream AWTRIX NG 1.2.2, a large restructure, is planned as separate work.

What this PR does

It adds support for the Pimoroni Galactic Unicorn, the 53×11 LED panel driven by a
Pico W (RP2040) or a Pico 2 W (RP2350). The Unicorn runs the same core as the ESP32
builds: the same HTTP and MQTT API, web UI, Home Assistant discovery and Berry scripting.
Along the way, every board also gains tall panels (8 to 16 rows).

ESP32 behaviour stays the same. The one exception is a small fix in commit 2 that the ESP32
shares: a refused upload no longer deletes the file that was already there.

The Unicorn has come up a few times on the AWTRIX Discord:

How to review it

This is a big PR, so it is arranged as milestones that can be reviewed one at a time. Each
of the 23 commits is one logical change and passes the full CI on its own. The first two
milestones (commits 1 to 7) do not depend on the Pico at all and could land separately.

Milestone Commits What it adds
1. Groundwork 1–3 A local CI container; the upload fix above; a simulator fix so it answers an oversized body with 413, like the device
2. Tall panels 4–7 Panel heights from 8 to 16 rows; eight-row content centred on taller panels; effects that fill the panel; BrickBreaker, Snake and PingPong as real mini-games
3. Pico builds 8–10 Galactic Unicorn builds running the core engine; build features and fixed GPIO; UF2 files in CI and releases; settings stored in LittleFS
4. The board 11–15 The panel driver (PIO and DMA); buttons and light sensor; Wi-Fi, provisioning and mDNS; NTP, sleep and UDP; the HTTP API and web UI
5. Robustness and integrations 16–20 The hardware watchdog; the buttonCallback webhook; API smoke and burst scripts; MQTT and Home Assistant discovery; melodies through the Unicorn's speaker
6. Polish 21–23 The web UI hides what a board lacks and shows fixed pins read-only; a Galactic Unicorn guide; Berry scripting on both Picos

About 8,100 of the changed lines are WebUiAsset.h, which is regenerated from
webui/index.html in the two commits that change the web UI (4 and 21).

How it was tested

  • Every commit passes the host tests, the three ESP32 builds, both Pico builds, the web UI
    tests, the docs checks and mkdocs build --strict, all through tools/container/ci-local.sh.
  • In the simulator, tools/pico/smoke.sh passes 96/0 and tools/pico/burst.sh 80/80.
  • On a real Galactic Unicorn with a Pico W:
    • API: smoke.sh passes 113/0 and burst.sh 80/80.
    • Hardware: the display, buttons, light sensor, melodies on the speaker, mirror and rotate (checked with a camera), and the mini-games at 40–42 fps.
    • Network: Wi-Fi on a mesh and on a single access point, provisioning, and MQTT with Home Assistant.
    • Scripting: a Berry script, a build without scripting, and the A+C rescue at power-on.
    • Robustness: watchdog recovery, and slow or stalled clients that no longer reset the board, including uploads, downloads, trickled requests, request headers split across packets, a header over 2 KB (answered 431), and a download the client stops reading (closed after 5 s).
    • Settings: backups with nested icon folders, a backup made on an ESP32 (the Unicorn keeps its own pins and panel), pin and panel edits that the board refuses, a timed sleep, and every web UI page as the board serves it.
  • The Pico 2 W build is checked in CI, the host tests and the simulator, but not yet on
    hardware. I have a Galactic Unicorn with a Pico 2 W on the way and will pick up
    #7 when it arrives. I don't think
    that should hold up this PR: the work left there is small next to what is here, and this PR
    is big enough already.

Known issues, tracked separately

These came up while building and reviewing this PR. They are tracked in
https://github.com/flavio-fernandes/awtrix-ng/issues rather than fixed here.

Already on main and unrelated to this PR:

  • #2 A web UI test (hub-script-updates) is occasionally flaky.
  • #3 A host test (test_asyncqueue) is occasionally flaky under load.
  • #4 A failed Wi-Fi join at boot reports timeout instead of the real reason.

Follow-ups for the Galactic Unicorn that don't block it:

  • #5 An upload over 2 KB sent deliberately slowly (under about 180 bytes per second) can still trip the watchdog. This is documented in limits.md.
  • #6 Scripts on the Pico cannot make HTTP or Modbus requests or draw icons yet. This is documented too.
  • #7 Run the Pico 2 W build on real hardware (see above).
  • #8 Two hardware checks still owed: the button webhook, and a power cut in the middle of a write.
  • #9 Some Pico boot messages go only to USB serial, not to the web UI log.

Checklist

  • New behaviour has a host test in test/, or it genuinely cannot run off-device
  • src/core/ stayed portable (no Arduino, FastLED or board headers)
  • Documentation in docs/ updated for any new field, endpoint or setting
  • Regenerated any checked-in generated file this change invalidates
    (see the table in CONTRIBUTING.md)

Thank you for taking the time to look at this!

🤖 Generated with Claude Code

@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 059fee9a76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/platform/rp2040/main_rp2040.cpp
Comment thread src/hal/GalacticUnicornBoard.h Outdated
Comment thread src/platform/rp2040/main_rp2040.cpp Outdated
Comment thread src/platform/rp2040/main_rp2040.cpp Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

try 2.
@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c628721ec8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/sim/SimHttpServer.cpp Outdated
Comment thread src/transport/http/HttpApiServer.cpp Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

try 3.
@codex review

Please review the whole PR (all 23 commits against main), not just the latest push, and be exhaustive in this one pass. I'd rather get every finding now than across several rounds.

  • Report everything you would raise at any priority (P0–P3), including things you would normally hold back for a later review.
  • If there are more findings than you post as inline comments, list the rest in the review summary with file:line.
  • For each finding, give the concrete failure scenario and which commit it belongs to.
  • Skip the six resolved threads unless the fix itself is wrong.
  • If you find nothing further, say so explicitly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c47f126e38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hal/GalacticUnicornDefaults.h Outdated
Comment thread src/transport/http/UpdateImage.h
Comment thread src/transport/http/HttpApiServer.cpp Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

try 4.
@codex review

Final pass before this goes upstream. Please review the whole PR (all 23 commits against main) once more and give a clear verdict.

  1. Check the nine resolved threads: is each fix correct and complete? Reopen any that is not.
  2. Report every remaining defect this PR introduces, at any priority, in this one review. Put anything beyond your inline comments in the summary with file:line. I want the complete list now, not spread across rounds.
  3. Only real defects: behaviour that is wrong for a user, a crash, data loss, a security issue, a broken build or test, or docs that contradict the code. Leave out style preferences and optional refactors.
  4. List problems that already exist on main and that this PR does not make worse separately, as "pre-existing". They won't block this PR.
  5. End with one line: either "No further findings: ready to merge" or "N findings remain".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40a6c57b4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/platform/rp2040/main_rp2040.cpp
Comment thread src/platform/rp2040/ScriptHeapRp2040.cpp
Comment thread docs/reference/http.md Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

Round 5, on fa4e7f2. Please review the whole PR (all 23 commits against main) and treat this as the final pass.

Already done, so please verify it rather than start over:

  • Pico watchdog: every HTTP request and response path, MQTT, the buttonCallback webhook, DNS, Wi-Fi reconnect, filesystem jobs and the USB serial log were checked, and the fixes were tested on hardware.
  • Docs: every section this PR adds or changes was checked against the final code, and each commit builds the docs with mkdocs --strict.
  • Known limit, documented in docs/reference/limits.md: an upload over 2 KB sent slower than about 180 B/s can still let the watchdog restart the Pico. Raise it only if you think that limit is wrong or badly documented.

Please:

  1. Confirm that the twelve resolved threads are fixed correctly. Reopen any that are not.
  2. Report every remaining defect this PR introduces, at any priority, in this single review: behaviour wrong for a user, a crash, data loss, a security issue, a broken build or test, or docs that contradict the code. No style or optional refactors.
  3. List anything that is already broken on main and not made worse here separately, as "pre-existing".
  4. If nothing is left, react with 👍 instead of commenting.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa4e7f26c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/transport/net/NetworkService.cpp
Comment thread docs/advanced/galactic-unicorn-display.md Outdated
Comment thread docs/reference/gpio.md Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

Round 6
Please review PR #1 again at head 4f403b7 (feat/galactic-unicorn, 23 commits).

What changed since your last review (previous head fa4e7f2, kept as branch galactic-unicorn.rc6):

  • Only documentation, in the commits that made each line stale: the Pico Wi-Fi retry (every 60 s, the AP and DNS stopped for one station-only join), the provisioning steps, the Galactic Unicorn guide, and gpio.md now saying 400 invalidPinConfig. There are no firmware changes; the galactic_unicorn build is byte-identical to fa4e7f2.
  • Your three round-5 threads are answered and resolved. The boot-join "timeout" reason also happens on main for the ESP32, so it is tracked separately.

Already tracked as issues, so please don't report these again unless this PR makes them worse:
#2 web UI test flake, #3 asyncqueue test flake, #4 boot join reports timeout, #5 a body over 2 KB trickled slower than ~180 B/s can trip the Pico watchdog (documented in limits.md), #6 script http.*/Modbus/icons missing on the Pico, #7 Pico 2 W not run on hardware, #8 webhook and power-loss hardware checks, #9 Pico boot messages bypass logf.

How to review, so this can be the last round:

  1. Report everything you find in this one pass, ranked by severity: correctness, watchdog/blocking, ESP32 regressions, then docs. Don't hold anything back for a later round.
  2. For each finding, give the file and line, a concrete failing scenario, and whether it is new in this PR or already on main.
  3. Don't reopen a resolved thread unless the fix is wrong, and if so, say what is still wrong.
  4. Skip style nits unless they hide a bug.
  5. If you find nothing that should block merging, say exactly: "No further findings at 4f403b7."

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f403b7ae1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/persistence/LittleFsRestoreSink.cpp Outdated
Comment thread webui/index.html
Comment thread src/system/DeviceServices.h
flavio-fernandes and others added 7 commits September 30, 2026 09:59
Runs the same gates as CI, locally, with no toolchain on the host. The
image has Python 3.12, Node 22, PlatformIO, CMake and Ninja; the runner
takes any mix of steps (native, webui, docs, mkdocs, build ENV..., all)
from the repository root and prints PASS or FAIL for each:

    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

A failed step does not stop the later ones; the exit code reports any
failure. Like the Docs workflow, the mkdocs step also fails on a link to
a missing anchor, which mkdocs --strict reports only at INFO level.
CONTRIBUTING.md points to it next to the list of CI gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device truncated the target at the first chunk and removed it when
the content check failed, so a refused replacement deleted a good icon.
The simulator checks before it writes. Upload beside the target and
rename it over only once the whole file has passed. LittleFS rename
replaces the target in one step, so the old file is never deleted
first and stays readable until the new one takes its place. Until then
both copies are on flash, and a power cut can leave the partial one as
<name>.part; http.md says both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device refuses an ordinary request body over 8 KB
(HttpApiServer::kMaxBodyBytes) with 413 payloadTooLarge; the simulator
accepted any size. Apply the same cap. Multipart uploads and script
source are streamed on the device, so they stay exempt here too; the
script routes come from api::isRawBodyWrite(), the predicate the device
uses, so installs and compare-and-swap updates are both covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds panelHeight (8-16, default 8) next to panelWidth and panels: config
rules, MatrixLayout, DeviceConfig, backups, the API docs and the web
UI's panel form. As with the total width, a height change applies after
a reboot; the firmware and the simulator only take a layout change live
while the canvas size is unchanged.

The height is stored under the NVS key "pheight". "ph" is a key of older
firmware that DeviceConfig::save() deletes, so it stays on the legacy
list and is never read.

The simulator sizes its canvas from the saved configuration at startup.
tools/test_panel_geometry.py checks that end to end, and the device test
reads the expected frame-buffer height from /api/v1/system. WebUiAsset.h
is regenerated from webui/index.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text, 8x8 icons, the built-in apps, the boot and AP screens and
notification text are all laid out for exactly eight rows. On a taller
panel they now render into an eight-row band that starts at row floor((H
- 8) / 2). render::contentBand() is an allocation-free, clipped view, so
every font keeps its baseline and clipping. Backgrounds, effects,
overlays, transitions and charts use the whole canvas, and progress bars
sit on the last row. Draw commands and explicitly positioned icons keep
absolute coordinates. The Eye effect scales to the full height.

The graphics, scripting, payload and limits docs describe the geometry.
test_contentband, test_fullcanvas (53x11) and new render-pipeline cases
cover it, and tools/test_tall_notification.py checks a notification in
the simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several effects assumed eight rows and left most of a tall panel dark.
Snake now follows a vertical serpentine path, so its trail crosses the
height quickly even on a wide panel. SwirlIn and SwirlOut use two
rotating elliptical spiral packets sized to the canvas instead of a
small clipped cluster in the middle. BrickBreaker's ball and wall also
cover the height; the next commit turns it into a real game. None of
them allocate per frame.

test_tall_effects checks every height from 8 to 16 at widths 32 and 53,
including determinism and guard sentinels around the frame buffer.
tools/sim/effect_survey.py starts an isolated simulator per panel size,
samples every effect from /api/v1/display/screen and writes PNG strips
and a summary; tools/sim/README-effects.md explains how to run it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On every panel size:

- BrickBreaker: 3-LED bricks with 1-LED gaps in 2 rows (3 from 11 rows
  up), one hue per row, one brick knocked out per hit, and 45-degree or
  shallow bounces picked by where the ball meets the paddle.
- Snake: a greedy snake chases a red food pixel, grows by one per meal
  and restarts when it is stuck or 24 long. The palette still colours
  the body.
- PingPong: tracking paddles on both edges and the same two return
  angles. The receiving paddle moves to its aim point, not just under
  the ball, so both angles occur, and corner returns go shallow instead
  of locking at 45 degrees.

All three replay at most 511 ticks from a fixed start, with fixed-size
state and no heap, so seeking and dropped frames stay deterministic.
test_tall_effects pins the brick geometry, one brick per hit, the ball
never overlapping a brick, both slopes, snake growth and food, and
paddles that cover the ball, at widths 32 and 53 and heights 8-16. The
effect reference, the release notes and the survey notes are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flavio-fernandes and others added 4 commits September 30, 2026 10:49
A, B and C are left, select and right in the shared PeripheryService,
with the same rotate, swapButtons and blockNavigation rules, double
press and button events as an Ulanzi. The brightness, volume and sleep
keys dispatch SetSettings and SetDisplay (GalacticUnicornControls), so
they behave exactly like the API. All keys are active low and debounced
for 35 ms. The light sensor on ADC2 feeds the shared auto-brightness
curve.

The list of built-in apps, effects and overlays moves from main.cpp into
core/BuiltinCatalog.h, so both platforms register the same five apps,
nineteen effects and six overlays. tools/check_builtin_registration.py,
run by CI's docs job and ci-local.sh docs, keeps it that way. The ESP32
button webhook moves into system/PeripheryHttp.h, so the shared
periphery has no network dependency.

test_galactic_inputs covers debounce, key steps and navigation parity;
test_builtin_catalog covers the catalog and the canonical overlay names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pico W runs the shared NetworkService: stored credentials, the boot
timeout, reconnect checks, the weak-signal roam policy, the provisioning
AP with captive DNS, and mDNS (<hostname>.local, _http._tcp and
_awtrixng._tcp with the same TXT records as the ESP32). Holding B for
one second at boot forces the AP without erasing credentials.
platform/rp2040/WifiCompat.h absorbs arduino-pico's differences: AP+STA
is restored after a join, and the static IP arguments come in a
different order.

arduino-pico brings the CYW43 radio up before setup(). RadioStartup
defers that with a linker --wrap until the display has claimed its PIO
and DMA, then logs which PIO state machines the radio took and checks
that the panel refresh still advances.

Several fixes came from running on real routers and mesh networks:

- Power save is turned off after every join. The CYW43 default (PM2)
  dropped the link seconds after joining; the ESP32 already uses
  WIFI_PS_NONE.
- Firmware roaming is turned off (roam_off=1) before every join. The
  radio roamed between mesh nodes on its own, and the roams back timed
  out in the key exchange. Roaming stays with roamIfWeak, as on the
  ESP32.
- A join is only restarted once the previous one has had its full
  connect timeout. arduino-pico's begin() does not block after
  WiFi.mode(), and the 5 s check kept aborting joins.
- The boot join is retried up to three times before falling back to the
  AP. After an unclean reset the AP can still hold the old session and
  deauthenticate the first join mid key exchange.
- CYW43 has one radio, so with the AP up the station can only join on
  the AP's channel. The Pico drops the AP for one station-only join
  window every 60 s while no phone is attached, and reboots once it
  joins.
- lastError reads badCredentials only when two joins in a row fail
  authentication (core/net/WifiLink.h AuthFailureGate). CYW43 reports a
  deauth in the middle of the handshake as BADAUTH, like a wrong
  password.
- Every CYW43 async event (type, status, reason, AP, join state) is
  logged at debug level through a --wrap of
  cyw43_cb_process_async_event, and link-up logs the AP and RSSI, so the
  device log says why a link dropped.

On a Pico W: 40 minutes on a mesh with no drops, then 60 minutes on a
single AP, reachable 60 times out of 60 with no rejoins. test_pico_wifi
and test_linkstatus cover the host-testable parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Time: the configured POSIX tz and ntpServer drive SNTP, which restarts
  when either changes or the link comes back. Pages use the same
  DevicePageClock as the ESP32.
- Reset reason: a platform ResetReason maps the RP2040's cause to the
  shared names (poweron, watchdog, software, external, brownout,
  unknown).
- Sleep: the Pico has no deep sleep. POST /api/v1/device/sleep blanks
  the panel, enables radio power saving and waits for the duration or a
  new press of the Sleep key, then restarts. The docs describe it as
  emulated.
- UDP: UdpSocket gains a WiFiUDP binding, so the shared discovery
  (4210/4211) and Art-Net (6454) services run unchanged.
  galactic_unicorn_udp_measure, a non-release environment without them,
  shows they cost 140 bytes of static RAM and about 2 KB of flash.
- Effect noise is seeded from the SDK's hardware entropy at boot.

DeviceServices uses the shared Preferences adapter. test_pico_system and
test_pico_udp cover the time config, reset mapping, UDP binding and the
Art-Net pixel mapping across the 53x11 canvas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HttpApiServer now also builds on arduino-pico's WebServer: its
HTTPServer& request hooks (including canRaw), bodies streamed into the
fixed arena, LittleFS's File API for the directory listings (the ESP32
keeps its VFS readdir, and melodies are read through readAsset on
both), and the asynchronous Wi-Fi scan lifecycle. The script-listing part of the state
JSON moves into core/script/ScriptInfo, so /api/v1/apps and
/api/v1/device work with no script host. /update answers 503 on the Pico
with instructions to flash a UF2 over USB; the simulator's 501 is
documented too.

Pushed apps and notifications draw their icon and icons: the Pico links
the shared GIF and JPEG decoders and the ESP32's DevicePageIcon, and
platform/rp2040/AssetFileRp2040.cpp reads assets through LittleFS, since
arduino-pico has no VFS. Uploading, deleting or restoring an asset drops
the cached icons and palettes, as on the ESP32. A mirror or rotate
change applies live: the panel size is fixed, so the board only ever
takes the orientation from the layout.

The Unicorn's panel is part of the board, like its pins, so
PUT /api/v1/system refuses a width other than 53, more than one panel,
a height other than 11 or 8, and any change to the wiring keys, with
422 validationFailed on the field; before, they were stored and
reported while the panel drew its own size. The rules come from the
board's SocProfile, so the ESP32 profiles are unaffected. A restored
backup keeps the Unicorn's pins and panel, as it keeps the Wi-Fi, so a
backup from a board wired differently still restores the rest.
The capabilities' gpio object names the panel ("panel": its width and
the heights it can run at). At boot, sysconfig::adoptFixedBoard()
replaces a pin map or panel size stored by another build with the
board's own; otherwise such a device would refuse every later
PUT /api/v1/system. test_galactic_board and test_pico_persistence
cover all three.

Fixes found on a Pico W:

- Link _printf_float. arduino-pico's PlatformIO builder leaves it out
  (its Arduino IDE platform.txt adds it), so every float in the JSON API
  rendered empty ("lightLevel":,) and the web UI fell back to
  provisioning mode.
- arduino-pico's lwIP has five TCP pcbs and gives accepted clients
  TCP_PRIO_MIN while the listener stays at TCP_PRIO_NORMAL. A SYN that
  found the pool full made lwIP abort the oldest live client, so two or
  three of the web UI's eight parallel requests failed with a reset. The
  listener now runs at its clients' priority, so the extra SYN is
  dropped and the client retries. lwIP reports that dropped SYN as
  accept(NULL, ERR_MEM), and WiFiServer would wrap the NULL pcb and
  fault, so it is filtered out first.
- fps is measured in the loop.
- largestFreeBlockBytes is the free block at the top of the heap. newlib
  cannot walk its free list, so the true largest block can only be
  larger; the device reference says so.

test_http_state checks the apps and device JSON without a script host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

Round 7
Please review PR #1 again at head 0980571 (feat/galactic-unicorn, 23 commits).

What changed since your last review (previous head 4f403b7, kept as branch galactic-unicorn.rc7):

  • Your three round-6 threads are fixed in the commits that introduced each problem, answered and resolved: nested asset folders in a restore, pages opened directly before the capabilities arrive, and the sleep docs.
  • A diff against main of every file this PR touches. ESP32 paths the PR had changed are back to main's code (directory listings, melodies, icon origins). The Unicorn's panel is now fixed like its pins: PUT /api/v1/system answers 422 for its width, panel count, height and wiring, and a restored backup keeps the board's own pins and panel. Shared reference docs now describe the Pico where they did not.
  • The Docs workflow failed on a link to a missing anchor. That is fixed, and ci-local.sh now fails on a missing anchor, as the Docs workflow does.
  • Web UI on a board with fixed wiring. The capabilities' gpio object gains "fixed" and "panel". The System page shows the Unicorn's pins read-only and drops the controls for hardware it lacks. The Unicorn's I2C pins are now -1, because nothing on the Pico reads I2C. At boot, a pin map or panel size stored by an earlier build is replaced by the board's own.
  • Every commit passes tools/container/ci-local.sh. The head was flashed on the Unicorn and tested there: smoke 113/0, burst 80/80, a restore from an ESP32-layout backup, refused pin and panel edits, and every web UI page as the board serves it.

Already tracked as issues, so please don't report these again unless this PR makes them worse:
#2 web UI test flake, #3 asyncqueue test flake, #4 boot join reports timeout, #5 a body over 2 KB trickled slower than ~180 B/s can trip the Pico watchdog (documented in limits.md), #6 script http.*/Modbus/icons missing on the Pico, #7 Pico 2 W not run on hardware, #8 webhook and power-loss hardware checks, #9 Pico boot messages bypass logf.

How to review, so this can be the last round:

  1. Report everything you find in this one pass, ranked by severity: correctness, watchdog/blocking, ESP32 regressions, then docs. Don't hold anything back for a later round.
  2. For each finding, give the file and line, a concrete failing scenario, and whether it is new in this PR or already on main.
  3. Don't reopen a resolved thread unless the fix is wrong, and if so, say what is still wrong.
  4. Skip style nits unless they hide a bug.
  5. If you find nothing that should block merging, say exactly: "No further findings at 0980571."

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0980571823

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/transport/http/HttpApiServer.cpp Outdated
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

Round 8
Please review PR #1 again at head 56564c1 (feat/galactic-unicorn, 23 commits).

What changed since your last review (previous head 0980571, kept as branch galactic-unicorn.rc9):

  • Only commit 16, feat(rp2040): reboot a hung Pico with the hardware watchdog, changed. Commit 23 was rebased and its content is unchanged. Your round-7 thread about the Pico request gate is fixed there, answered and resolved.
  • The gate now reads the request head into a 2 KB buffer that WebServer's parser reads back from (HeadClient), because WiFiClient::peekBytes() only sees the first TCP segment. A head longer than 2 KB gets 431. The head scan lives in transport/http/RequestHead.h and has a host test. limits.md, errors.md and the display guide document the limit.
  • Every commit passes tools/container/ci-local.sh. The head was flashed on the Unicorn and tested there: split and long heads are answered, a 2.1 KB head gets 431, and trickled heads and bodies are dropped with no watchdog reset. smoke.sh and burst.sh pass.

Already tracked as issues, so please don't report these again unless this PR makes them worse:
#2 web UI test flake, #3 asyncqueue test flake, #4 boot join reports timeout, #5 a body over 2 KB trickled slower than ~180 B/s can trip the Pico watchdog (documented in limits.md), #6 script http.*/Modbus/icons missing on the Pico, #7 Pico 2 W not run on hardware, #8 webhook and power-loss hardware checks, #9 Pico boot messages bypass logf.

How to review, so this can be the last round:

  1. Report everything you find in this one pass, ranked by severity: correctness, watchdog/blocking, ESP32 regressions, then docs. Don't hold anything back for a later round.
  2. For each finding, give the file and line, a concrete failing scenario, and whether it is new in this PR or already on main.
  3. Don't reopen a resolved thread unless the fix is wrong, and if so, say what is still wrong.
  4. Skip style nits unless they hide a bug.
  5. If you find nothing that should block merging, say exactly: "No further findings at 56564c1."

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56564c1607

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/transport/http/HttpApiServer.cpp
flavio-fernandes and others added 8 commits September 30, 2026 16:40
Arm the RP2040 watchdog at 8 s once the radio is up, and feed it on
every loop() pass, in the boot Wi-Fi join's wait callback and while
sleeping. A factory reset disarms it before formatting LittleFS.

A whole HTTP request and response happen inside one loop() pass, and
arduino-pico's WebServer waits on the client with timeouts that restart
whenever a byte moves, so a slow client could hold the pass past 8 s.
On the Pico:
- every upload, restore and raw script-source callback feeds it,
  including the ones that refuse a feature-disabled route (/update,
  MP3 uploads), which still read the whole body before answering 503;
- a request is parsed only once its header, and a body of up to 2 KB,
  has fully arrived; a client that has not sent it within 5 s is
  dropped. The header is read into a 2 KB buffer that WebServer's
  parser then reads back (HeadClient): WiFiClient::peekBytes() sees
  only the first TCP segment, so a header that spans segments, or
  arrives in two writes, could not be checked where it lies. A longer
  header is answered 431 and closed;
- every response byte goes through RawWebServer's client-write hooks,
  which write only what the send buffer has room for and feed the
  watchdog while waiting; a client that takes nothing for 5 s is given
  up on and its connection closed, so the rest of the response, such as
  a file sent 1 KB at a time, is skipped instead of each piece waiting
  out its own 5 s. Files go out through sendContent(), since
  streamFile() writes past those hooks.
Other boards keep their existing paths. transport/http/RequestHead.h
finds where the header ends however the reads split it, and reads its
Content-Length; test_http_state covers both.

The reset reason is read once, before the watchdog is armed; read
later, it reported an ordinary reboot, such as after flashing, as
"watchdog".

Verified on a Pico W with a build that hangs on purpose: the watchdog
rebooted it after 8 s and /api/v1/device reported resetReason
"watchdog". A slow /update upload, a slow download of the web UI and
of a 30 KB file, and a header or small body trickled a byte every 2 s
all reset the board before these changes. Now each finishes or is
dropped, as do slow MP3 and script uploads, and the board keeps running.
Requests whose header is sent in two writes or runs to 1.9 KB are
answered; a 2.1 KB header gets 431 at once, sent whole or trickled.
A 40 KB file whose reader stops taking it held the loop for as long as
the reader stayed connected (40 s in the test); it is now closed about
5 s after the last byte went out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PeripheryHttp.h is now shared by the ESP32 and the Pico, with 300 ms
connect and response timeouts on both, so an unreachable listener only
stalls the display briefly. The body is built by a portable, host-tested
helper (core/api/ButtonEventJson.h), identical to an ESP32's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/pico/smoke.sh makes about a hundred HTTP API calls against a
device or native_sim and prints PASS or FAIL for each. It reads
boardType and /api/v1/capabilities to expect the documented answer where
targets legitimately differ (503 for a compiled-out feature, 503 for
/update on the Pico and 501 in the simulator), undoes every write, and
never prints configuration.

tools/pico/burst.sh fires the eight requests the web UI makes on load at
once, and exits non-zero on anything but a 200 within 45 s. This is the
load that exposed the lwIP connection resets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pico links the shared MqttService, HaAnnouncer and MqttLink, so
topics, commands, retained state and discovery are the same as on an
ESP32.

- Broker names: arduino-pico resolves hosts, including .local through
  lwIP mDNS, only with blocking calls. HostResolverPico bounds each
  lookup to 1 s, and to 2 s with the bare-label .local fallback
  (BoundedHostLookup).
- arduino-pico applies the client timeout to TCP connect and write too,
  so the MQTT client uses 300 ms there.
- Discovery streams many bounded TCP writes and feeds the watchdog
  between them.
- The Home Assistant device is named "AWTRIX NG" unless a hostname is
  set, the same rule as on the ESP32.

test_picohostlookup covers the resolver's bounds and fallback, and
test_hadiscovery checks that the Unicorn keeps the stock device identity
and button entities. test/integration/mqtt_smoke.py (run_mqtt_smoke.sh
starts mosquitto in podman) drives native_sim through discovery,
retained state, commands, button press and release edges and the last
will. docs/reference/mqtt.md describes the Pico's lookup and timeout
bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Unicorn has an I2S amplifier and speaker instead of a buzzer.
core/sound/ToneSamples renders RTTTL as 24 kHz, 16-bit square waves with
the buzzer's pitch, note and rest lengths and 6 ms gap between notes.
platform/rp2040/UnicornToneSink streams them through arduino-pico's I2S
(GPIO 9/10/11) with non-blocking writes, so audio never blocks the loop
or the watchdog. The amplifier enable (GPIO 22) is held low from boot
and whenever nothing plays or the volume is zero.

The sink is the shared buzzer tone sink: "sound" plays a saved melody,
"soundRtttl" inline notes, and buzzerVolume and the volume keys scale
the output. /api/v1/capabilities reports the buzzer and no MP3, track or
radio. If the PIO or DMA resources are missing, the amplifier stays
muted and the capability is left out.

test_tone_samples covers sample generation, stop and replacement, live
gain, and the real notification router picking a saved melody or inline
RTTTL with tone-only capabilities. Heard on a Pico W: the "alert"
melody, the volume keys and idle mute, with Wi-Fi and the display
running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web UI now reads "scripting" from /api/v1/capabilities, next to the
audio sinks it already read. Without it, the Scripts tab and the Run
scripts setting are hidden and a "Not available on this board" note is
shown. The audio page names a missing MP3 or radio output the same way.
Until the capabilities arrive everything counts as present, so nothing
flickers away on a full build. The navigation is rebuilt once they
arrive, and each page that hides something waits for them before it
draws, so a page opened directly by its address is right too.

On a board with fixed wiring (gpio.fixed), the System page lists the
board's pins and reserved ranges instead of offering them, keeps only
the panel height (one of gpio.panel's heights) and the orientation, and
leaves out what the board cannot have: the DFPlayer toggle, the battery
and I2C sensor settings and LDR on GND, and, on the Display page, the
sensor colours. With no piezo pin but a speaker, the melody slider is
called Melody volume. The note that the select button cannot wake a
deep sleep is left out, and the sound switch mentions radio only where
there is one. Every page that reads the capabilities now also takes
gpio from them.

Browser firmware install is limited to the three ESP32 images. A board
that reports another updateImage, a Pico's .uf2, gets no Download &
install, which could only fail, and no .bin upload, which /update
refuses. The update row still names a newer release and links its
notes, where the .uf2 is, and the Maintenance section explains the USB
(BOOTSEL) route instead.

webui/test/capabilities.test.js covers the capabilities, including
the audio and system pages opened directly while /device and
/capabilities are still slow to answer (the harness can now open a
route and delay answers), a fixed board against the same board
reported as configurable (the harness can now serve /api/v1/system and
/api/v1/display), and update-check.test.js both Pico images.
WebUiAsset.h is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/advanced/galactic-unicorn.md covers choosing the image, flashing
over BOOTSEL, provisioning, the buttons, which features each Pico build
has, emulated sleep and the tall-panel rule. README, the release notes,
the device, GPIO and limits references and the mkdocs navigation link to
it.

UF2 targets use the framework's linker layout, not an ESP-IDF partition
table. tools/check_partitions.py now also checks every environment's
LittleFS reservation, including inherited ones, and
test/test_partition_reservations.py covers the checker; CI's docs job,
ci-local.sh docs, CONTRIBUTING.md and building.md all include it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripting is on in both Pico builds. AWTRIX_FEATURE_SCRIPTING=0 still
builds it out, with the capability, routes and web UI following. The
Pico environments compile core/script, ScriptStore and Berry, and
main_rp2040 wires the script host the way main.cpp does, without script
HTTP requests and icons, which need ESP32-only workers: http.* and
Modbus reads, which share that request path, return false and icons are
not drawn. ScriptStore now persists scripts.
scriptingEnabled now defaults to true on the Unicorn as on every board;
the false default dated from before the Pico could run scripts. A build
with AWTRIX_FEATURE_SCRIPTING=0 reports scriptHeapPool "unavailable"
with a zero budget, as documented.

- ScriptHeapRp2040 gives the VM its own budget from the one heap that
  Wi-Fi, HTTP, MQTT and the display share: 48 KB on the Pico W and 96
  KB, the ESP32's, on the Pico 2 W (AWTRIX_RP2040_SCRIPT_HEAP_KB).
- AsyncQueue uses a no-op lock where std::mutex does not exist.
  arm-none-eabi newlib has no gthreads, and every producer there runs on
  the main loop. Other targets keep std::mutex.
- ScriptStore opens the script directory with an explicit "r".
  arduino-pico's FS::open has no default mode; "r" is the ESP32 default.
- Holding A and C (left and right) for three seconds at power-on turns
  scripting off and shows NOSCR, the ESP32's rescue combo for a script
  that takes the device down on every boot.
- Script output goes through the shared log buffer, so print() and debug
  lines reach /api/v1/logs and the web UI as on the ESP32.

Each call into a script is capped at BerryVM::kInstructionLimit and
returns to loop(), which feeds the watchdog. smoke.sh now installs,
reads back, lists and deletes a script where scripting is available. The
guide, limits, persistence and API docs describe scripting on both
builds.

On a Pico W: the interpreter costs about 19 KB of free heap with no
script, a Hello script runs, and smoke.sh and burst.sh pass. The Pico 2
W is verified by build, host tests and the simulator only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flavio-fernandes

Copy link
Copy Markdown
Owner Author

@codex review

Round 9
Please review PR #1 again at head 3b8b18d (feat/galactic-unicorn, 23 commits).

What changed since your last review (previous head 56564c1, kept as branch galactic-unicorn.rc10):

  • Only commit 16, feat(rp2040): reboot a hung Pico with the hardware watchdog, changed. Commits 17-23 were rebased and their content is unchanged. Your round-8 thread is fixed there, answered and resolved: when a response write makes no progress for 5 s, writeFed() now closes the connection, so the remaining pieces of a file, listing or JSON stream return at once.
  • I also re-read every wait and loop in the Pico-specific code for anything else that could hold loop() or starve the watchdog: the request and response paths, the button webhook, the MQTT handshake, the Wi-Fi join, sleep, the display and the tone sink. I found nothing more to change.
  • Every commit passes tools/container/ci-local.sh. The head was flashed on the Unicorn and tested there. A 40 KB download the client stops reading now leaves the board silent for about 5 s after the last progress; before, it was silent for as long as the client stayed connected. The round-7 request-head checks, smoke.sh and burst.sh pass.

Already tracked as issues, so please don't report these again unless this PR makes them worse:
#2 web UI test flake, #3 asyncqueue test flake, #4 boot join reports timeout, #5 a body over 2 KB trickled slower than ~180 B/s can trip the Pico watchdog (documented in limits.md), #6 script http.*/Modbus/icons missing on the Pico, #7 Pico 2 W not run on hardware, #8 webhook and power-loss hardware checks, #9 Pico boot messages bypass logf.

How to review, so this can be the last round:

  1. Report everything you find in this one pass, ranked by severity: correctness, watchdog/blocking, ESP32 regressions, then docs. Don't hold anything back for a later round.
  2. For each finding, give the file and line, a concrete failing scenario, and whether it is new in this PR or already on main.
  3. Don't reopen a resolved thread unless the fix is wrong, and if so, say what is still wrong.
  4. Skip style nits unless they hide a bug.
  5. If you find nothing that should block merging, say exactly: "No further findings at 3b8b18d."

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 3b8b18d126

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@flavio-fernandes
flavio-fernandes marked this pull request as draft September 30, 2026 22:26
@flavio-fernandes flavio-fernandes self-assigned this Sep 30, 2026
flavio-fernandes and others added 5 commits October 6, 2026 12:55
The Galactic Unicorn with a Pico 2 W (RP2350) now runs on real hardware:
display at 42 fps, mirror/rotate, notifications, light sensor, Wi-Fi,
mDNS, NTP, the HTTP smoke suite, watchdog recovery and settings kept
across UF2 updates. Measured: the interpreter costs about 17 KB of free
heap and a script of about 28 KB of source installs before the 96 KB
budget runs out. Drop "compile-only" from the README, the guide, the
limits page and the release notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked on a real Galactic Unicorn with a Pico 2 W running 0b50eca: A/B/C
events, brightness, volume and Sleep keys, inline and stored melodies
heard at two volumes, and the A+C NOSCR rescue turning scripting off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In provisioning mode the Pico pauses its setup network every minute to
try the saved Wi-Fi. arduino-pico's station-only begin() stops only the
station, so the setup network kept running, and the softAP() that
resumed it failed on the running interface; its rollback took the setup
network down. After the first failed retry there was no setup network,
logged as `provisioning AP ... at (IP unset)`. The boot path was fine.
Stop the setup network before the retry and before each start.

The core also prints an unset address as "(IP unset)" where ESP32
prints "0.0.0.0". /api/v1/device passed that on as ipAddress, so the web
UI did not recognise setup mode: a phone on the setup network got the
dashboard instead of the Wi-Fi form. Report "0.0.0.0" instead.

Seen on a Pico 2 W Unicorn. A bench build whose joins always fail showed
the setup network at 192.168.4.1 at boot and with no address after the
first retry; with the fix it is at 192.168.4.1 every time, and a phone
saved Wi-Fi through the form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A phone joined the setup network, got the Wi-Fi form and saved the
network, on the image with the setup-network fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant