Skip to content

Cyberdeck demos, games, LED panel; ESP-IDF v6.0.1 migration#11

Open
Afiyetolsun wants to merge 34 commits intofirefly:mainfrom
Afiyetolsun:feat/cyberdeck-mega-v6
Open

Cyberdeck demos, games, LED panel; ESP-IDF v6.0.1 migration#11
Afiyetolsun wants to merge 34 commits intofirefly:mainfrom
Afiyetolsun:feat/cyberdeck-mega-v6

Conversation

@Afiyetolsun
Copy link
Copy Markdown

@Afiyetolsun Afiyetolsun commented May 9, 2026

Summary

Single coherent PR landing the cyberdeck demo suite (4 panels), four
games, a 12-mode LED panel, build/flash helpers, and an
ESP-IDF v5.4 → v6.0.1 migration. After this, a fresh
git clone --recurse-submodules && ./build.sh && ./flash.sh boots
end-to-end on a Pixie DevKit rev.6 against the pinned submodules.

All new code uses only the documented firefly-scene and
firefly-hollows public API (with one inline forward-decl block in
the LED panel for the still-private pixels_animatePixel). No new
image assets — every new panel is composed from boxes and labels.

New panels

Cyberdeck demos

Panel File Mechanic
Cyber Pulse panel-cyber.c 8 neon equalizer bars (per-bar triangle-wave oscillator, unique phase + period), translucent CRT scanline sweep
Life Grid panel-life.c 16×16 Conway's Game of Life, toroidal wrap, 4 seeds (glider, pulsar, R-pentomino, random)
Byte Stream panel-bytes.c 12-column Matrix-style falling-glyph rain, opacity-faded trails, riding glyph per head
Sys Stats panel-stats.c Live uptime / free heap / render FPS / chip rev / IDF version / build hash, plus a 60-sample free-heap timeline

Games

Game File Mechanic
Raycast panel-raycast.c Wolf3D-style first-person raycaster following Lode Vandevenne's tutorial (public domain, credited in source). 16×16 map, 60 vertical strips at 4 px wide, fish-eye-corrected, side-shaded for fake lighting, sliding collision.
Asteroids panel-roids.c Side-scroll dodger/shooter. Drifting rocks come from the right with random velocities and screen-bouncing. Hold N/S to dodge, OK to fire, X to exit.
Breakout panel-brick.c Vertical Breakout — 5×10 brick wall on the left with a rainbow palette, paddle on the right, ball deflection angle scales with hit position relative to paddle center.
Dungeon panel-crawl.c Turn-and-step grid crawler in the EotB style. 12×12 dungeon, find the green goal cell to escape; step counter doubles as the score.

LED Mode panel — 12 modes

panel-leds.c drives the four WS2812B LEDs through firefly-hollows'
internal pixels API. Each mode is a PixelAnimationFunc bound to
each pixel via pixels_animatePixel(..., repeat=1, ...) so it loops
forever in the IO task and persists after the panel is popped
set a vibe and walk away.

# Mode Effect
0 OFF all off
1 CYAN solid neon cyan
2 RAINBOW cycling hues, per-LED phase offsets
3 PULSE violet breathing
4 STROBE 1 Hz white flash
5 POLICE alternating red/blue pairs
6 MATRIX green wave
7 FIRE red/orange flame flicker (per-pixel hash)
8 COMET bright yellow head chases the 4-LED ring
9 OCEAN blue/cyan ripple, per-LED phase
10 NEON rotating magenta/cyan/yellow/green
11 SPARKLE random twinkle, ~20 % lit at any moment

A few notes on the pixels API used:

  • pixels_animate (all-pixels) and pixels_stopAnimation are
    declared in pixels.h but not implemented in pixels.c. This
    panel only uses pixels_animatePixel (the per-pixel variant) and
    passes the LED index via arg. The relevant symbols are private,
    so they're forward-declared inline.
  • The repeat parameter is a boolean — 0 is one-shot,
    non-zero loops. The boot animation in task-io.c passes 0,
    which is why the boot LEDs settle on a static endpoint; this panel
    passes 1 so modes loop.

Menu

panel-menu.c rewritten for a scrolling viewport (3 visible rows,
12 entries). Top/bottom neon rules consistent with the cyberdeck
theme. The previously-disabled GIFs entry is re-enabled.

Build / flash helpers

  • build.sh — wraps the documented Docker one-liner with a
    preflight check, submodule init, IDF-image pinning
    (espressif/idf:v6.0.1 default, override with IDF_IMAGE), and
    auto-cleaning of build/ when the image changes (so switching
    major.minor versions doesn't reuse an incompatible cmake cache).
  • flash.sh — auto-detects /dev/tty.usbmodem* (macOS) /
    /dev/ttyACM*+/dev/ttyUSB* (Linux); accepts -p PORT/-b BAUD
    and ESPPORT/ESPBAUD. Prefers host esptool.py
    (one pip install esptool) over the Docker --device flag, which
    doesn't work on macOS Docker Desktop. --monitor resolves through
    idf.pyesp-idf-monitor → Docker (Linux only) → screen,
    prints the Ctrl+] exit hint up front.

ESP-IDF v6 migration

Submodule pins bumped to upstream main to pick up the v6 fixes:

  • firefly-display: c65494d → 0f9183fUpdate for ESP-IDF v6
  • firefly-hollows: 2fbda9d → c856a8fUpdate BLE API for
    ESP-IDF v6
    , Updates for printf errors in ESP-IDF v6, Update for
    LED strip IRAM config for ESP-IDF v6
  • firefly-scene: 6ae2319 → 210b4d1 — label rendering improvements

Plus two sdkconfig changes:

  • CONFIG_RMT_{TX,RX}_ISR_CACHE_SAFE=y — picked up from the
    pixie-hello-world
    reference repo. In v6 the RMT driver's default ISR callback
    location is no longer cache-safe, so flash operations were able to
    glitch the WS2812B output.
  • CONFIG_BT_NIMBLE_DEBUG=nBLE_HS_DBG_ASSERT in NimBLE
    asserts ble_hs_locked_by_cur_task() at the top of
    ble_hs_id_addr, but the bookkeeping path that would register the
    owning task on a ble_hs_lock() acquisition isn't reached during
    the host-init flow. With BT_NIMBLE_DEBUG=y the device
    panic-reboots right after onSync is invoked. Disabling it (the
    standard production setting) compiles those debug asserts out and
    lets BLE init proceed normally.

sdkconfig was regenerated by IDF v6.0.1; most of the diff is
auto-generated SoC capability flags reshuffling rather than
functional changes.

Bonus: build-was-broken-on-main fixes that v6 made transparent

Side-effect of the submodule bumps: every build-time patch that
earlier cuts of this branch had been carrying is now gone, since the
upstream main commits address them directly:

  • task-ble.c was missing #include "hollows.h" (so TaskBleInit
    was undeclared);
  • hollows.c:132 was missing a trailing comma in the TaskBleInit
    designated initializer (so .ready parsed as a member access on
    the integer version);
  • panel-tx.c referenced COLOR_BACK, which is not a defined
    symbol — replaced with COLOR_NAVONLY (the documented
    navigation-button color in firefly-hollows.h);
  • main.c called ffx_init with three args, but the API takes four
    since the firmware-version field was added.

These were all hard build failures or boot-loops on a clean
firefly/pixie-firmware:main checkout. They're worth flagging only
because anyone trying to clone-and-build today hits them; this PR
gets the project back to a state where the documented one-liner
actually works.

Test plan

  • ./build.sh produces build/pixie.bin cleanly on macOS
    Docker Desktop with espressif/idf:v6.0.1. Binary is 6.3 MB,
    ~10 % free in the 7 MB factory partition.
  • ./flash.sh --monitor flashes and the device boots cleanly to
    the menu on a Pixie DevKit rev.6.
  • All 12 menu entries launch and return via Cancel.
  • LED modes loop forever and persist after exiting the panel.
  • Build verified on Linux with native ESP-IDF v6.0.1
    (untested locally — no Linux box at hand).

Notes for the maintainer

  • Happy to split this into smaller PRs if it's easier to review —
    e.g., (1) v6 migration + sdkconfig fixes, (2) cyberdeck demos,
    (3) games, (4) LED panel. Whichever shape you prefer.
  • The scrolling menu is currently slot-based (items snap between
    fixed Y positions, hidden when off-viewport). When the
    clipping-bounds work lands, swapping the menu over to a single
    scrolling group inside a clip rect will be a one-liner — happy to
    follow up with that PR once it ships.
  • LED panel's forward-declared pixels_animatePixel is the only
    thing reaching into firefly-hollows's private API. Once the
    pixel API is finalized in the public header (the // @TODO: Pixel API block in firefly-hollows.h) I'll drop the forward decls.

Afiyetolsun added 30 commits May 9, 2026 00:20
…stats)

Adds four new menu entries that showcase the firefly-scene engine and
turn the Pixie into a cyberpunk-styled cyberdeck:

* Cyber Pulse  - 8 neon equalizer bars driven by a triangle-wave
                 oscillator with per-column phase/period offsets,
                 plus a translucent CRT-style scan line.
* Life Grid    - 16x16 Conway's Game of Life with toroidal topology.
                 Four seeds (glider, pulsar, R-pentomino, random),
                 N/S to switch seed, OK to pause/step, Cancel to exit.
* Byte Stream  - Matrix-style falling-character stream, 12 columns
                 with random period/phase, opacity-faded trails, and
                 a single glyph riding each head.
* Sys Stats    - Live system telemetry (uptime, free heap, render FPS,
                 chip rev, IDF version, build hash) plus a 60-sample
                 free-heap timeline. Uses ffx_sceneLabel_setTextFormat
                 for dynamic value updates.

Menu refactor:
* panel-menu now supports a scrolling viewport (3 visible / 7 items),
  with the previously-disabled GIFs entry re-enabled.
* Top/bottom neon rules added for cyberpunk styling consistent with
  the new panels.

Other:
* main.c: pass firmware version to ffx_init (required by the
  current pinned hollows submodule API; main was previously
  out-of-sync with components/firefly-hollows).
* panels.h: add declarations for the new panels and fix
  pushPanelGifs return type (int, matching its definition).

All new code uses only the documented firefly-scene / firefly-hollows
public API. No new image assets - everything is composed from boxes,
labels and the existing arrow sprite.
build.sh wraps the documented Docker one-liner from README.md, with
preflight checks for docker availability and submodule init.

flash.sh handles the full flash without Docker passthrough quirks:
* auto-detects /dev/tty.usbmodem* (macOS) and /dev/ttyACM*/ttyUSB*
  (linux); -p PORT, -b BAUD, ESPPORT/ESPBAUD env vars also honored
* prefers host esptool.py (works without ESP-IDF installed); falls
  back to native idf.py if available
* writes bootloader, partition table and pixie.bin at the correct
  offsets matching partitions.csv
* --monitor flag opens a serial monitor after flashing
espressif/idf:latest now ships ESP-IDF 6.1, which fails to bootstrap
on a project last configured against 5.4.1 (component manager looks
for files that don't exist yet on a fresh 6.x build). Pin to the
project's actual IDF version. Override with IDF_IMAGE env var.
panel-tx.c referenced COLOR_BACK in three places (lines 125, 237, 338)
for the 'BACK' info-button color, but no such symbol exists in the
pinned firefly-color.h. The semantically-correct replacement is
COLOR_NAVONLY, which firefly-hollows.h documents as the back-button
color. Same upstream-vs-submodule drift as the ffx_init signature.

Also bump the default Docker image from v5.4.1 to v5.5.4 - the latest
v5.5 line builds the project cleanly and is better-supported.
Switching ESP-IDF minor versions (5.4 -> 5.5) leaves an incompatible
cmake cache pointing at the previous python_env, producing a 'currently
active env doesn't match' error. Track the image used for the last
build in build/.idf-image and wipe build/ on mismatch.
The pinned firefly-hollows commit (2fbda9d) has a real bug:
src/task-ble.c references TaskBleInit (defined in src/hollows.h) without
including it, so any clean build of this project fails inside the
submodule.

The next upstream commit (1f55b89) adds the missing include, but the
same commit also reshuffles the FfxKey bit assignments. Inheriting that
without a coordinated downstream update is a regression risk we want to
keep out of this PR.

build.sh now applies a narrow, idempotent in-place patch to add the
missing include before invoking docker. Real fix belongs upstream as a
PR against firefly/component-hollows pulling out just the include
change.
components/firefly-hollows/src/hollows.c line 132 reads:

    TaskBleInit init = {
      .version = version       <-- missing comma
        .ready = xSemaphoreCreateBinaryStatic(&readyBuffer)
    };

Without the comma the compiler parses '.ready' as a member access on
the integer 'version', producing 'request for member ready in something
not a structure or union' and the unused-but-set-parameter error on
'version'. Same upstream-bug class as the missing include - apply an
idempotent in-place fix from build.sh.
screen reads raw bytes and prints panic addresses unsymbolicated, which
is useless for debugging boot-time crashes. When neither host idf.py nor
esptool but docker is available, run the monitor inside the same pinned
IDF container - it reads build/pixie.elf and resolves PCs to source
locations.
ESP-IDF v5.5's NimBLE tightened locking on ble_hs_id_copy_addr - it
asserts the caller holds the host lock and panics otherwise. The
pinned firefly-hollows task-ble.c calls it bare from onSync(), so the
device boot-loops with:

  assert failed: ble_hs_id_addr ble_hs_id.c:295 (ble_hs_locked_by_cur_task())

Add ble_hs_lock()/ble_hs_unlock() around the call via build.sh patch.
ble_hs_lock and ble_hs_unlock exist in NimBLE's lib but are not in the
public ble_hs.h on this version, so add inline forward declarations.
Use 'extern void ble_hs_lock' as the idempotency sentinel so re-runs
of build.sh don't double-wrap the call.
Docker Desktop on macOS runs the engine in a Linux VM, so the
--device flag can't reach host /dev/tty.usbmodem* paths and the
container exits with 'no such file or directory'. esp-idf-monitor is a
standalone pip package that does the same panic symbolication as
'idf.py monitor' without needing the full IDF.

Resolution order is now native idf.py, then esp-idf-monitor, then
docker (Linux only), then screen as last resort.
The previous patch only wrapped ble_hs_id_copy_addr, but
ble_hs_id_infer_auto on the line above also goes through ble_hs_id_addr
internally and hits the same lock-held assert first. Move the lock to
cover the entire id-resolution sequence in onSync.
Replaced the (incorrect) host-lock wrapper patch with an early
vTaskDelete in taskBleFunc, just after the bootstrap semaphore is
given. NimBLE in IDF v5.5 has an internally inconsistent BLE_HS_DEBUG
configuration where BLE_HS_DBG_ASSERT is active but the
ble_hs_lock_nested bookkeeping that would mark the owning task is
gated differently, so ble_hs_locked_by_cur_task always returns false
and the assert in ble_hs_id_addr fires right after the BLE host task
starts.

The cyberdeck demos run entirely on the display/keypad path and do not
need BLE. The wallet panel needs it but is broken at this commit
regardless. Real fix belongs upstream as a NimBLE/IDF-version-aware
update of firefly-hollows.
Stacks four new games on top of the cyberdeck-suite branch:

* Raycast    - Wolf3D-style first-person raycaster following Lode
               Vandevenne's tutorial algorithm (public domain, credited
               in the source). 16x16 map, 60 strips at STRIP_WIDTH=4
               rendered as boxes with per-frame setSize, fish-eye-
               corrected wall heights, side-shaded coloring.
               Controls: N=forward, S=turn-left, OK=turn-right, X=exit.
               Sliding collision so walls don't trap you.
* Asteroids  - side-scroll dodger/shooter. Drifting rocks come from the
               right with random velocities, bouncing off top/bottom
               edges. Tap OK to fire bullets, hold N/S to dodge.
               Score on survival and rock kills, OK restarts on death.
* Breakout   - vertical Breakout. 5x10 brick wall on the left with a
               rainbow palette, paddle on the right. Ball deflection
               angle depends on hit position relative to paddle center.
               OK launches/restarts, N/S move paddle.
* Dungeon    - turn-and-step grid crawler. 12x12 dungeon, classic
               EotB-style controls (N=turn-left, S=turn-right,
               OK=step). Find the green goal cell to escape; step
               counter doubles as the score.

Menu refactor: ITEM_COUNT bumped 7 -> 11, scrolling viewport handles
the four new entries automatically.

All four games are pure scene-graph (no new image assets), use only
the documented firefly-scene API, and pass clang -fsyntax-only.
Adds 'LED Mode' menu entry that drives the 4 WS2812B LEDs through
the firefly-hollows pixels API. Seven modes:

* OFF       - all LEDs off
* CYAN      - solid neon cyan
* RAINBOW   - cycling hues, per-LED phase offsets
* PULSE     - violet breathing
* STROBE    - 1 Hz white flash
* POLICE    - alternating red/blue pairs
* MATRIX    - green wave traveling across the four LEDs

Each mode is a PixelAnimationFunc kicked off via pixels_animate(),
which the IO task ticks in the background, so the selected mode
keeps running after the panel is popped - the user can set a vibe
and walk away. Mode changes stop the previous animation and start
the new one.

The pixels API is private to firefly-hollows (header in src/), so
the relevant symbols are forward-declared inline (same pattern used
elsewhere in this branch for ble_hs_lock). Animations use the
existing fixed_ffxt math helpers (scalarfx, mulfx, tofx) and HSV
color helpers - no math.h dependency.

ITEM_COUNT bumped 11 -> 12 in the menu.
esp_idf_monitor (and idf.py monitor) treat Ctrl+C as data forwarded
to the target rather than an exit signal; the actual quit shortcut
is Ctrl+]. Print a banner up front so users don't get stuck in the
monitor wondering why their interrupt is being ignored.
The header pixels.h declares pixels_animate (all-pixels) and
pixels_stopAnimation, but pixels.c at the pinned commit only
implements the per-pixel variant pixels_animatePixel - the linker
correctly fails with 'undefined reference' on the missing two.

Switch the LED panel to drive each of the four LEDs individually:
each animation function writes only out[0], the pixel index travels
in via the 'arg' parameter, and applyMode loops 0..3 calling
pixels_animatePixel once per LED. Re-calling pixels_animatePixel
overwrites the previous animation slot for that pixel, so mode
switching works without an explicit stop call.
…arkle)

* Pass repeat=1 (not 0) to pixels_animatePixel. The implementation
  treats repeat as a boolean - 0 was one-shot, so modes were running
  for one duration cycle and then freezing on the last frame.
  Confirmed at firefly-hollows/src/pixels.c:381:
    context->actions[pixel].type = repeat ? AnimationTypeRepeat
                                          : AnimationTypeNormal;
  Counter-intuitive naming, but pinning the API.

* Add 5 new modes:
  - FIRE     - red/orange flame flicker via per-pixel hash
  - COMET    - bright yellow head chases around the 4-LED ring
  - OCEAN    - blue/cyan ripple, slow phase-offset per LED
  - NEON     - rotating magenta/cyan/yellow/green palette
  - SPARKLE  - random twinkle, ~20% on at any time

12 modes total. Index dots shrunk to 14px to fit the wider strip.
Drops the entire stack of v5-era submodule patches (5 of them in
build.sh: missing include, missing comma, COLOR_BACK, BLE host lock,
NimBLE BLE_HS_DEBUG bypass) by bumping all three vendored components
to their upstream main, which has explicit IDF v6 fixes:

* firefly-display:  c65494d -> 0f9183f  ('Update for ESP-IDF v6')
* firefly-hollows:  2fbda9d -> c856a8f  ('Update BLE API for ESP-IDF v6',
                                         'Updates for printf errors in
                                          ESP-IDF v6', 'Update for LED
                                          strip IRAM config for ESP-IDF
                                          v6', and the missing-include
                                          and missing-comma fixes that
                                          we previously patched in)
* firefly-scene:    6ae2319 -> 210b4d1  (label rendering improvements)

Also bumps the default Docker image from v5.5.4 to v6.0.1, the
latest stable release in the v6.0.x line. sdkconfig regenerated by
IDF v6 (mostly noise: SoC-capability flags reshuffled).

Build verified clean on macOS Docker Desktop. pixie.bin is 6.3 MB
(10%% free in the 7 MB factory partition). The build.sh auto-clean
sentinel handles the v5 -> v6 cmake-cache transition automatically.
NimBLE's BLE_HS_DBG_ASSERT macro gates lock-state assertions on
MYNEWT_VAL(BLE_HS_DEBUG), which the IDF Kconfig wires to
CONFIG_BT_NIMBLE_DEBUG. With debug on, ble_hs_id_addr asserts that
ble_hs_locked_by_cur_task() returns true. In practice the
bookkeeping path that would register the owning task on a lock
acquisition is in a different code path than the one the assert
inspects, so the assert always fails right after onSync. The device
panic-reboots on every IDF version we've tried (v5.5.4 and v6.0.1).

Disabling BT_NIMBLE_DEBUG turns the BLE_HS_DBG_ASSERT macro into a
no-op (the same set of asserts compile out together), letting BLE
init proceed normally. This is the correct production setting -
NimBLE deployments don't run with the host-debug bookkeeping in
production builds.
Per upstream maintainer feedback, in IDF v6 the RMT driver's default
ISR callback location is no longer cache-safe. With WS2812B LED
control on the RMT TX path, this means flash operations (BLE NVS
writes, the wallet panel touching attest data, etc.) can starve or
glitch the LED-strip ISR. The pixie-hello-world reference repo
explicitly enables these:

  CONFIG_RMT_TX_ISR_CACHE_SAFE=y
  CONFIG_RMT_RX_ISR_CACHE_SAFE=y

so the ISR is placed where it stays callable while the flash cache
is busy. Match that here.
Hollows hard-codes FfxDisplayRotationRibbonRight (MADCTL bits MV+MX:
90-degree page/column swap plus horizontal mirror), which on the
rev.6 hardware produces a horizontally-flipped display - text reads
backwards in the menu and panels.

Patch task-io.c via build.sh to use FfxDisplayRotationRibbonBottom
instead (operand 0, no MADCTL flips). Idempotent so re-runs are
safe. Real fix belongs upstream as a board-revision-aware default
in firefly-hollows.
Le Space (panel-space.c):
* 5 progressive levels with increasing alien grid (4x3 -> 6x5),
  decreasing bullet capacity (5 -> 4), and faster sprite animation
  cadence on later levels.
* Per-level alien field stride to slow movement on level 1.
* Score: 10 * level per kill, 100 * level bonus per level cleared.
* GameState state machine: Splash -> Playing -> LevelClear -> next
  level (or Victory after L5) / GameOver on death.
* Hold-to-quit (OK 3s) preserved; Cancel still fires.
* GAME OVER / VICTORY overlays accept OK to restart at L1, Cancel
  to return to menu.

Asteroids (panel-roids.c):
* Background, ship, bullets and rocks now use the Le Space sprite
  set (image_space, image_ship, image_bullet, image_alienboom).
  Rocks come from the LEFT and drift right toward the ship; ship
  is on the right firing left, matching the directional sprites
  from panel-space.c so nothing renders backwards.
* Cancel now matches the Le Space convention (fire), with second
  Cancel-press exiting after game-over.
* GAME OVER overlay; OK restarts.

Image data centralization (images.c + image-data.h):
* The image-data headers under main/images/ define their arrays at
  file scope without 'static', so including the same one from two
  panels produced multiple-definition link errors. Move all the
  inclusions into a single new TU (main/images.c) and expose
  extern declarations + sizeof()-derived size constants via the
  new main/image-data.h. image_background and image_pixie are
  deliberately omitted; firefly-hollows ships its own copy of those
  symbols in src/demo/background-pixies.c and including ours would
  collide at link time.
Both action games are now in classic-arcade portrait orientation,
with the ship at the bottom, threats descending from the top, and
bullets firing up. N/S map to left/right ship movement.

Le Space:
- Ship horizontally centered along the bottom edge.
- Alien grid descends from the top of the playable area below the
  HUD; the field oscillates left/right as it advances down.
- Bullets travel up; collision boxes use ALIEN_W/2 and ALIEN_H/2
  centered on each alien.
- Death is now alien y plus half-height reaching the ship row,
  not the ship column.

Asteroids:
- Ship at the bottom; rocks drift down from the top, bouncing off
  the left and right edges instead of top and bottom.
- Bullets travel up.
- Long-press OK (3s) now exits to the menu, matching Le Space.

New game (panel-snake.c):
- 16x16 grid (14px cells, 224x224 board centered with HUD on top).
- Snake starts length 4 facing east. Tap a direction to turn.
  N goes up, S goes down, OK goes right, Cancel goes left. Long-
  press OK 3s exits to the menu; on game-over OK restarts and
  Cancel exits.
- U-turns are filtered (they would self-collide on the next tick).
- Snake render uses a fixed pool of MAX_SNAKE=96 segment box nodes,
  positioning visible ones to body cells and hiding the rest.
- Eating food grows the snake by 1, accelerates the move tick from
  180ms toward a 70ms floor, and bumps the score by 10.
- PERFECT screen on board fill; GAME OVER on wall or self
  collision.

Menu now has 13 entries; ITEM_COUNT bumped accordingly.
The ship, alien1, alien2 and alien-boom sprites were drawn for the
original horizontal Le Space layout (ship on the right firing left).
After flipping both action games to a portrait layout (ship at the
bottom firing up) the sprites were oriented sideways relative to
their direction of travel.

Add a runtime sprite rotator in images.c that walks the
firefly-scene RGB565+A4 image format:

  data[0]  = type/format flag (0x05 for RGB565+A4)
  data[1]  = width
  data[2]  = height
  data[3]  = alphaCount   (uint16_t alpha words; 4-bit alpha/pixel)
  data[4..]= alpha bitmap, then RGB565 pixels (row-major)

For each rotated copy we malloc a fresh buffer of the same total
length, swap width and height, and rewalk both pixel and alpha
arrays remapping new(nx,ny) <- old(ny, oldH-1-nx). Allocation is
once at boot via images_initRotated(), invoked from app_main
before the panels are pushed; the rotated copies live in heap for
the lifetime of the app (about 7.5 KB total).

Le Space and Asteroids now reference image_X_cw / image_X_cw_len
instead of the originals. Sprite-size constants in both panels
also swap accordingly: ALIEN goes from 22x26 to 26x20, SHIP from
36x38 to 38x36, alien grid spacing tightened to 32x28 to match.
Adds two small UX touches across every panel.

LED feedback (feedback_onKey):
- Each press fires a one-shot pixels_animatePixel on the LED next
  to the pressed button: North=LED0, OK=LED1, Cancel=LED2,
  South=LED3.
- Animation is a brief low-brightness pale-cyan flash that fades to
  black, ~140ms total. repeat=0 makes it one-shot so the LED lands
  on black after.
- Hooked into every panel's onKeys (menu, space, roids, snake,
  cyber, life, bytes, stats, raycast, brick, crawl). Skipped in
  panel-leds, which manages its own pixel animations and would
  immediately overwrite the flash anyway.
- Pixel API symbols are forward-declared inline (same pattern used
  in panel-leds.c) since they live in firefly-hollows' private
  src/ header.

Button legend (feedback_addButtonLegend):
- Helper that draws a small bottom-of-screen strip in the format
  '< NORTH  > SOUTH  OK ACTION  X CANCEL' so each panel can show
  what its four buttons do in the current context.
- Added to the menu (UP/DN/GO/EXIT) and to the two action games
  Le Space and Asteroids (L/R/HOLD=EXIT/FIRE) which previously had
  no on-screen hints.
- Other panels keep their existing custom hint labels - the helper
  is available if they ever want to switch to the canonical
  format.
The previous default of 460800 was conservative for hardware
flow-controlled UART. The ESP32-C3 on the Pixie talks over the
built-in USB-Serial/JTAG, where the nominal baud is just a token
to esptool - actual throughput is bounded by USB and the host CDC
driver. 921600 is a safe higher value that roughly halves the
flash time on every system tested.

Also expose --app-only, which writes only build/pixie.bin at
offset 0x10000 and skips the bootloader and partition table.
Bootloader and partition table only change when sdkconfig or the
partitions.csv / IDF version changes; for the typical 'I edited a
panel and want to reflash' loop, --app-only cuts roughly a third
off the total time.
The previous legend at FfxFontMedium with format
'< NORTH  > SOUTH  OK ACTION  X CANCEL' was around 26 chars at
12 px each, ~310 px wide on a 240 px screen, so the leading
'< NORTH ' and trailing 'X EXIT' got clipped past the edges.

Switch to FfxFontSmall (15-point glyphs are roughly 7-8 px wide)
and a tighter format '<UP >DN OK:GO X:EXIT' that fits comfortably
inside 240 px. Game legends shorten 'HOLD=EXIT' to 'HOLD=X' to
keep the line balanced.
The Pixie has only four keys; the previous legend's '<' and '>'
prefixes (visual stand-ins for North/South) were just noise. Reword
the legend format to enumerate the four button names directly:

  UP:up  DN:dn  OK:go  X:exit

Each panel still passes its own action verbs; the helper builds the
final string. Game legends adjusted to 'L', 'R', 'hold=exit', 'fire'
to read consistently in the new format.

Add a small running FPS counter in the top-left corner of the menu.
Helper FpsCounter struct and feedback_addFpsCounter / feedback_tickFps
functions live in feedback.c so any panel that wants the same widget
can drop in three lines (one struct field, one init call, one tick
call from its render handler). Updates roughly once per second based
on render-event count over the previous window.
…UP OK ESC

Hollows itself draws a built-in FPS label at (235, 235) - the
bottom-right corner of every frame. With the per-panel FPS counter
already exposed via feedback.c the duplicate is just clutter, and
in some panels it overlapped the button-legend strip. Patch
task-io.c via build.sh to park the label off-screen at
(-200, -200) - it still gets formatted once per second but never
draws. Idempotent.

Reorder the button legend format to match the device's physical
button row, left-to-right SW4..SW1:

  DOWN:%s UP:%s OK:%s ESC:%s

(was 'UP:%s DN:%s OK:%s X:%s'.) Helper signature unchanged - the
caller still passes (up, down, ok, cancel); the format string just
reorders them. Game legends compacted from 'hold=exit' to 'hold'
to fit comfortably at FfxFontSmall (~30 chars at 240 px wide).
Three small UX cleanups around the LED button-press feedback.

Legend format simplified to a plain space-join of four labels in
physical-button order: 'DOWN UP OK ESC' for the menu and
'DOWN UP HOLD FIRE' for the action games. Dropped the 'DOWN:%s'
prefix - callers now pass exactly what they want shown.

LED feedback brightness reduced to ~10% of the previous peak
(48 -> 5 on the RGB ramp). The pale-cyan flash is still visible at
arm's length but no longer competes with whatever else is on the
LEDs.

Feedback flash now only fires when the LED Mode panel's selected
mode is OFF (mode 0). panel-leds.applyMode calls the new
feedback_setLedOffMode(state->mode == 0) on every selection so the
gate updates immediately. Default state at boot is 'off mode = true'
so first-boot button presses still flash; the moment the user picks
a non-OFF mode, feedback steps out of the way until they switch
back to OFF.
Snake, Raycast, Breakout and Dungeon were still using their own
ad-hoc bottom-of-screen hint labels. Replace each with the shared
feedback_addButtonLegend so the format is consistent across the
whole device:

  Snake     -> DOWN UP RIGHT LEFT
  Raycast   -> LEFT FWD RIGHT EXIT
  Breakout  -> DOWN UP LAUNCH EXIT
  Dungeon   -> RIGHT LEFT STEP EXIT

The order on screen is the physical button row left-to-right
(SW4..SW1: DOWN UP OK ESC); each game passes the action verb that
maps to that physical button.

Also swap the LED feedback mapping for DOWN and ESC. The previous
mapping had FfxKeyCancel -> LED2 and FfxKeySouth -> LED3, but on
hardware the LED that physically sits above the DOWN button is
LED2, not LED3, so pressing DOWN was lighting up the LED above the
ESC button and vice versa. Final mapping reflects the physical
layout above each button, left-to-right SW4..SW1.
User reports SW2 (OK) lit the correct LED but the other three were
wrong. With LED 1 confirmed above SW2, the only consistent layout
is that LEDs are numbered right-to-left when looking at the front
of the device:

  LED 0 -> SW1 (ESC)
  LED 1 -> SW2 (OK)        <- already correct
  LED 2 -> SW3 (UP)
  LED 3 -> SW4 (DOWN)

Update the per-key mapping accordingly. The previous left-to-right
guess assumed LED 0 lived above DOWN; only SW2 happened to land
right by coincidence.
Replace each panel's ad-hoc bottom-of-screen hint label with the
shared feedback_addButtonLegend so the layout matches the games
and the menu. Always renders four labels in physical button order
(SW4 SW3 SW2 SW1 = DOWN UP OK ESC), with '-' for buttons that
have no action in the current panel.

  LED Mode  -> NEXT PREV RESET EXIT
  Cyber     -> -    -    -     EXIT
  Life      -> NEXT PREV PAUSE EXIT
  Bytes     -> -    -    -     EXIT
  Stats     -> -    -    -     EXIT

panel-gifs is left as-is - its right-side menu already labels what
each button selects, so an extra bottom strip would duplicate it.
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