Skip to content

Cyberdeck games + LED modes (Raycast, Asteroids, Breakout, Dungeon)#10

Open
Afiyetolsun wants to merge 18 commits intofirefly:mainfrom
Afiyetolsun:feat/cyberdeck-games
Open

Cyberdeck games + LED modes (Raycast, Asteroids, Breakout, Dungeon)#10
Afiyetolsun wants to merge 18 commits intofirefly:mainfrom
Afiyetolsun:feat/cyberdeck-games

Conversation

@Afiyetolsun
Copy link
Copy Markdown

Summary

Stacks on top of #N (cyberdeck-suite) and adds five more menu
entries: four games and a 12-mode LED panel that drives the four
WS2812B pixels. Until #N merges, this PR's diff includes all the
suite changes too.
Review #N first; this branch only adds five
new files and three small wiring edits on top.

New games (4)

All four are pure scene-graph (no new image assets), use only the
documented firefly-scene API, and pass clang -fsyntax-only clean.

  • Raycast (panel-raycast.c) — Wolfenstein 3D-style first-person
    raycaster following Lode Vandevenne's tutorial (public domain,
    credited in source). 16x16 map, 60 vertical strips at 4px wide
    rendered as scene boxes with per-frame setSize, fish-eye-corrected
    wall heights, side-shaded coloring (Doom-y red palette). Sliding
    collision so walls don't trap you. N=forward, S=turn-left,
    OK=turn-right, X=exit.
  • Asteroids (panel-roids.c) — side-scroll dodger/shooter.
    Drifting rocks come from the right with random velocities,
    bouncing off top/bottom edges. N/S=move, OK=fire (and
    restart on death), X=exit. Score increments on survival and
    on rock kills.
  • Breakout (panel-brick.c) — vertical Breakout: 5x10 brick
    wall on the left with a rainbow palette, paddle on the right.
    Ball deflection angle scales with hit position relative to paddle
    center. N/S=paddle, OK=launch/restart, X=exit.
  • Dungeon (panel-crawl.c) — turn-and-step grid crawler in the
    Eye-of-the-Beholder style. 12x12 dungeon, find the green goal cell
    to escape; step counter doubles as the score. N=turn-left,
    S=turn-right, OK=step, X=exit.

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 offset
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
10 NEON rotating magenta/cyan/yellow/green
11 SPARKLE random twinkle, ~20% lit at any moment

Note on the pixels API: the pixels_animate (all-pixels) and
pixels_stopAnimation symbols declared in pixels.h are not
implemented in pixels.c at the pinned commit, so this panel uses
the per-pixel pixels_animatePixel and passes the pixel index via
arg. Also: 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 boot LEDs settle on a static endpoint; this panel passes 1
so modes loop forever.

The relevant pixels symbols live in firefly-hollows' private src/
header, so they're forward-declared inline (same pattern this branch
uses for ble_hs_lock).

Menu

panel-menu.c's ITEM_COUNT bumped from 7 to 12. The scrolling
viewport from #N handles the additional entries automatically.

Test plan

  • ./build.sh produces build/pixie.bin cleanly.
  • ./flash.sh --monitor flashes and the device boots to the menu.
  • All five new entries (LED Mode, Raycast, Asteroids, Breakout,
    Dungeon) launch and return to menu via Cancel.
  • LED modes loop forever and persist after exiting the panel.
  • Raycast: turning is responsive, wall heights look right,
    sliding collision works.
  • Breakout: paddle responsive, ball physics feel right, brick
    collisions clean, lives counter decrements, restart works.
  • Asteroids: rocks spawn, dodgeable, bullets hit, game-over
    restarts on OK.
  • Dungeon: navigation reads well, goal reachable, win-screen
    appears with step count.

Afiyetolsun added 18 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.
@ricmoo
Copy link
Copy Markdown
Member

ricmoo commented May 9, 2026

Looks awesome! Up install it on a device and try it out myself a bit.

Thanks for tracking me down today to show me too.

I’m adding clipping-bounds to the FfxNode objects too, so it can have nice smooth scrolling menus, where text gets clipped at the boundaries too. ;)

There is a hello-world firmware repo too, which has some fixes for esp-idf v6. It addresses the RMT library changing the default location (ISR) for its callbacks.

@Afiyetolsun
Copy link
Copy Markdown
Author

Looks awesome! Up install it on a device and try it out myself a bit.

Thanks for tracking me down today to show me too.

I’m adding clipping-bounds to the FfxNode objects too, so it can have nice smooth scrolling menus, where text gets clipped at the boundaries too. ;)

There is a hello-world firmware repo too, which has some fixes for esp-idf v6. It addresses the RMT library changing the default location (ISR) for its callbacks.

Thanks Richard! Was great chatting today.

Stoked to hear the clipping-bounds work is coming — that'll make
the scrolling menu in this PR feel a lot nicer. Right now the
overflow rows just show until they slide off the visible area; a
real clip rect on the menu container would be a clean drop-in.

Pointer to pixie-hello-world was super useful — I just opened a
follow-up branch (feat/cyberdeck-mega-v6) that:

  • Bumps the three vendored components to their main to pick up your
    v6 fixes (Update for ESP-IDF v6, Update BLE API for ESP-IDF v6,
    Update for LED strip IRAM config for ESP-IDF v6, Updates for printf errors in ESP-IDF v6).
  • Pins the Docker image to espressif/idf:v6.0.1 (and bakes an
    auto-clean of build/ when the image changes, since switching
    IDF major.minor reuses an incompatible cmake cache).
  • Picks up CONFIG_RMT_{TX,RX}_ISR_CACHE_SAFE=y from the
    pixie-hello-world reference sdkconfig — the RMT default
    callback-location change you mentioned bit us when flash got busy.
  • Disables CONFIG_BT_NIMBLE_DEBUG (that's what was causing the
    ble_hs_id_addr host-lock assert at boot — BLE_HS_DBG_ASSERT
    was firing despite the lock-bookkeeping path not being taken).

Net result: clean build + clean boot end-to-end against pinned
submodules on a fresh clone, and the v5-era build-time submodule
patches I'd been carrying are all gone.

If you'd rather review the cyberdeck demos and the v6 migration
separately, happy to split. Otherwise everything's stacked on
feat/cyberdeck-mega-v6. Have fun with 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.

2 participants