Repository navigation
feat: Pimoroni Galactic Unicorn (Pico W / Pico 2 W) support and tall panels - #69
Closed
flavio-fernandes wants to merge 25 commits into
Closed
flavio-fernandes wants to merge 25 commits into
flavio-fernandes wants to merge 25 commits into
Conversation
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>
Pimoroni's Galactic Unicorn is a fixed 53x11 RGB panel driven by a Raspberry Pi Pico W (RP2040) or Pico 2 W (RP2350). This adds the galactic_unicorn and galactic_unicorn_2w environments on the arduino-pico core, with the platform pinned to a tested commit and 512 KiB reserved for LittleFS. This first step only brings up the portable core on the Pico: main_rp2040.cpp creates the CoreEngine and the RenderPipeline with the Time and Date apps and an unset clock, and GalacticUnicornBoard is a 53x11 board (8 rows letterboxed) with no display output yet, registered in BoardRegistry. The environments list their sources explicitly, so no ESP transport, persistence, Berry, MP3, TLS or OTA code is linked; the ESP32 environments leave the rp2040 sources out. test_galactic_board covers the board's geometry and its missing sensors and sinks. CONTRIBUTING.md and docs/advanced/building.md list the new environments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build can now leave out optional subsystems, and says so instead of failing in odd ways. FeatureSet (portable) and platform::buildFeatures() (from the AWTRIX_FEATURE_* macros, all on unless a build turns them off) cover scripting, MP3, radio, outbound TLS and browser OTA. featurePolicy() answers 503 unavailable for their HTTP routes and MQTT commands before any body is read, and /api/v1/capabilities reports the set, so clients never infer features from the chip. The ESP32 builds are unchanged: everything stays on. The Pico builds turn all five off for now. rp2040Profile() describes the Unicorn's fixed wiring. A fixedWiring profile rejects GPIO changes and still validates its own defaults. Its I2C pins are -1, like the battery, buzzer and DFPlayer pins: the Qw/ST connector is on GPIO 4/5, but the Pico builds read no I2C sensor. The capabilities' gpio object gains "fixed", true for such a board and false on the ESP32 chips. Choosing the active SoC profile moves from src/core/SocProfile.h to src/platform/SocProfile.cpp, and the FastLED data-pin list to src/platform/MatrixPins.h, so src/core keeps no chip #ifdefs. CI builds both UF2s and uploads them as uf2-galactic_unicorn and uf2-galactic_unicorn_2w, and ci-local.sh all builds them too. A tagged release also waits for them and publishes firmware-galactic-unicorn.uf2 and firmware-galactic-unicorn-2w.uf2 next to the ESP32 images. CONTRIBUTING.md and building.md list the new CI gate. The HTTP reference and OpenAPI document the 503 for a feature a build leaves out, and gpio.fixed. test_features covers the policy, the capabilities and the fixed pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arduino-pico has no NVS. KvPreferences implements the subset of the ESP32 Preferences API that DeviceConfig and NvsSettings use, over one LittleFS file per namespace. A namespace is committed at end() by writing a temporary file and renaming it, so a power cut leaves either the old or the new file. platform/Preferences.h picks it on RP2040 and the ESP32 Preferences elsewhere. Numbers wider than four bytes are rejected, not truncated. The backup-restore sink moves out of HttpApiServer into persistence/LittleFsRestoreSink, shared by both platforms. It stages each asset and renames it only when complete, so an aborted restore leaves no partial files. It accepts the paths the shared backup policy does, and creates every folder a nested entry such as /ICONS/sub/smile.gif needs. On the Pico, IconOriginsStore checks icons through the LittleFS File API, since arduino-pico has no VFS; the ESP32 keeps its VFS check. Scripts and radio stations get no-op stores while those features are compiled out. A requested reboot saves pending settings first, except after a factory reset. The configuration that applies live is applied at boot too: debugMode (verbose logging) and tempDecimals. Custom palettes in /PALETTES are found by name: the ESP32's LittleFS palette loader moves into persistence/PaletteFiles, which both boards install once the filesystem is mounted, and it now opens the directory with an explicit "r" because arduino-pico's open() has no default mode. docs/advanced/pico-persistence.md describes the layout. test_kv_preferences covers the store, including corruption and failed replacement. test_pico_persistence runs the production DeviceConfig, NvsSettings, LittleFS adapter and restore sink against a simulated filesystem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports the display half of Pimoroni pico v1.23.0 (MIT; see LICENSES/MIT-Pimoroni.txt and THIRD-PARTY-NOTICES.md). One PIO state machine and two chained DMA channels refresh fourteen binary-weighted bit planes continuously, with no CPU refresh interrupt. show() packs the inactive of two 9,240-byte buffers and swaps them at a frame boundary. The PIO program prefers PIO1, leaving PIO0 instruction memory for the CYW43 radio. Colour grading follows NG's order: saturation, gamma, brightness, correction, tint. It goes through a 14-bit LUT, so precision is not lost to an 8-bit intermediate, and it applies NG's gamma once in place of Pimoroni's fixed 2.2 curve. panelHeight defaults to 11; 8 is letterboxed on physical rows 1-8. The wiring is fixed, but mirror and rotate are drawing transforms, so the board applies them while packing the frame, from boot on. Without that, rotate would swap the buttons while the picture stayed upright. Portable defaults live in GalacticUnicornDefaults.h, so the host tests need no hardware headers. test_galactic_display covers bit-plane packing, mirror and rotate, and the colour LUT. docs/advanced/galactic-unicorn-display.md documents the refresh and colour design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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
force-pushed
the
feat/galactic-unicorn
branch
from
October 6, 2026 12:45
ebe7af6 to
3b8b18d
Compare
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>
Blueforcer
force-pushed
the
main
branch
2 times, most recently
from
October 6, 2026 17:09
f04b7b0 to
e25a3d6
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
buttonCallbackwebhook; API smoke and burst scripts; MQTT and Home Assistant discovery; melodies through the Unicorn's speakerAbout 8,100 of the changed lines are
WebUiAsset.h, which is regenerated fromwebui/index.htmlin the two commits that change the web UI (4 and 21).How it was tested
tests, the docs checks and
mkdocs build --strict, all throughtools/container/ci-local.sh.tools/pico/smoke.shpasses 96/0 andtools/pico/burst.sh80/80.smoke.shpasses 113/0 andburst.sh80/80.mirrorandrotate(checked with a camera), and the mini-games at 40–42 fps.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
mainand unrelated to this PR:hub-script-updates) is occasionally flaky.test_asyncqueue) is occasionally flaky under load.timeoutinstead of the real reason.Follow-ups for the Galactic Unicorn that don't block it:
limits.md.Checklist
test/, or it genuinely cannot run off-devicesrc/core/stayed portable (no Arduino, FastLED or board headers)docs/updated for any new field, endpoint or setting(see the table in CONTRIBUTING.md)
Thank you for taking the time to look at this!
🤖 Generated with Claude Code and Codex