diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c3f43ad --- /dev/null +++ b/build.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +if ! command -v docker >/dev/null 2>&1; then + echo "error: docker not found in PATH" >&2 + echo " install Docker Desktop: https://docs.docker.com/get-docker" >&2 + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + echo "error: docker daemon is not running" >&2 + echo " start Docker Desktop and try again" >&2 + exit 1 +fi + +if [ ! -f components/firefly-hollows/include/firefly-hollows.h ]; then + echo "==> Initializing submodules" + git submodule update --init --recursive +fi + +# Workaround for a bug in the pinned firefly-hollows commit (2fbda9d): +# src/task-ble.c references TaskBleInit (defined in src/hollows.h) without +# including it, so the build fails. Upstream fixed this in commit 1f55b89, +# but that commit also reshuffles the FfxKey bit assignments, which is too +# risky to inherit without also updating downstream code. Patch in place; +# idempotent so re-runs are safe. Real fix: open PR against +# firefly/component-hollows to land just the include change. +hollows_ble="components/firefly-hollows/src/task-ble.c" +if [ -f "$hollows_ble" ] && ! grep -q '^#include "hollows.h"' "$hollows_ble"; then + echo "==> patching $hollows_ble (add missing #include \"hollows.h\")" + awk ' + /^#include "firefly-tx.h"/ && !patched { + print + print "" + print "// Patched by build.sh: pinned commit is missing this include" + print "#include \"hollows.h\"" + patched = 1 + next + } + { print } + ' "$hollows_ble" > "$hollows_ble.tmp" && mv "$hollows_ble.tmp" "$hollows_ble" +fi + +# Same root cause - hollows.c:132 has a malformed initializer; the +# `.version = version` line is missing a trailing comma, so the next +# `.ready` field is parsed as a member access on `version`. +hollows_c="components/firefly-hollows/src/hollows.c" +if [ -f "$hollows_c" ] && grep -q '^[[:space:]]*\.version = version$' "$hollows_c"; then + echo "==> patching $hollows_c (add missing comma in TaskBleInit init)" + sed -i.bak 's/^\([[:space:]]*\)\.version = version$/\1.version = version,/' \ + "$hollows_c" && rm -f "$hollows_c.bak" +fi + +# IDF v5.5's NimBLE has an internally-inconsistent BLE_HS_DEBUG config: +# the BLE_HS_DBG_ASSERT macro is active and asserts ble_hs_locked_by_cur_task, +# but the lock-bookkeeping in ble_hs_lock_nested that would set the owning +# task handle never runs in this code path, so the assert always fails +# right after the BLE host task starts. The cyberdeck demos don't need +# BLE; bypass the host init by exiting taskBleFunc right after it signals +# ready. Wallet panel won't work, but it's broken on this commit anyway. +if [ -f "$hollows_ble" ] && \ + ! grep -q 'PIXIE-PATCH: skip-ble-init' "$hollows_ble" && \ + grep -q '^ xSemaphoreGive(init->ready);$' "$hollows_ble"; then + echo "==> patching $hollows_ble (skip BLE host init for demo build)" + awk ' + /^ xSemaphoreGive\(init->ready\);$/ && !patched { + print $0 + print "" + print " // PIXIE-PATCH: skip-ble-init - bypass NimBLE host init," + print " // which trips an internally-inconsistent BLE_HS_DEBUG" + print " // assert in ble_hs_id_addr on IDF v5.5." + print " vTaskDelete(NULL);" + patched = 1 + next + } + { print } + ' "$hollows_ble" > "$hollows_ble.tmp" && mv "$hollows_ble.tmp" "$hollows_ble" +fi + +# Pin the IDF image - `espressif/idf:latest` (6.x) fails to bootstrap +# on this project. v5.5.x is the most recent line known to build cleanly. +# Override with IDF_IMAGE if you know better. +IDF_IMAGE="${IDF_IMAGE:-espressif/idf:v5.5.4}" + +# Switching IDF major.minor versions reuses incompatible cmake cache +# (different python_env path). Wipe build/ when the image changes. +if [ -d build ] && [ -f build/.idf-image ]; then + cached=$(cat build/.idf-image) + if [ "$cached" != "$IDF_IMAGE" ]; then + echo "==> IDF image changed ($cached -> $IDF_IMAGE); cleaning build/" + rm -rf build + fi +fi + +echo "==> Building Pixie firmware ($IDF_IMAGE)" +docker run --rm \ + -v "$PWD":/project \ + -w /project \ + -e HOME=/tmp \ + "$IDF_IMAGE" idf.py build + +mkdir -p build && echo "$IDF_IMAGE" > build/.idf-image + +echo +echo "==> Build complete" +if [ -f build/pixie.bin ]; then + ls -lh build/pixie.bin +fi +echo +echo "Flash with: ./flash.sh" diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..15e5404 --- /dev/null +++ b/flash.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +usage() { + cat <&2; usage >&2; exit 2 ;; + esac +done + +if [ -z "$PORT" ]; then + for p in /dev/tty.usbmodem* /dev/cu.usbmodem* /dev/ttyACM* /dev/ttyUSB*; do + if [ -e "$p" ]; then PORT="$p"; break; fi + done +fi + +if [ -z "$PORT" ]; then + echo "error: no serial port detected" >&2 + echo " pass -p /dev/your-port or export ESPPORT=/dev/your-port" >&2 + echo " on macOS try: ls /dev/tty.usbmodem*" >&2 + echo " on linux try: ls /dev/ttyACM* /dev/ttyUSB*" >&2 + exit 1 +fi + +if [ ! -f build/pixie.bin ]; then + echo "error: build/pixie.bin not found - run ./build.sh first" >&2 + exit 1 +fi + +echo "==> Flashing $PORT @ ${BAUD}" + +if command -v esptool.py >/dev/null 2>&1; then + esptool.py --chip esp32c3 -p "$PORT" -b "$BAUD" \ + --before default_reset --after hard_reset \ + write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m \ + 0x0 build/bootloader/bootloader.bin \ + 0x8000 build/partition_table/partition-table.bin \ + 0x10000 build/pixie.bin +elif command -v idf.py >/dev/null 2>&1; then + idf.py -p "$PORT" -b "$BAUD" flash +else + echo "error: neither esptool.py nor idf.py found in PATH" >&2 + echo " install one of:" >&2 + echo " pip install esptool" >&2 + echo " or set up ESP-IDF: https://docs.espressif.com/projects/esp-idf/" >&2 + exit 1 +fi + +echo +echo "==> Flash complete" + +if [ "$MONITOR" = "1" ]; then + if command -v idf.py >/dev/null 2>&1; then + exec idf.py -p "$PORT" monitor + elif python3 -c 'import esp_idf_monitor' >/dev/null 2>&1; then + # Standalone monitor (pip install esp-idf-monitor). Reads pixie.elf + # and resolves panic PCs to source locations - same UX as idf.py + # monitor, no ESP-IDF or Docker needed. + exec python3 -m esp_idf_monitor -p "$PORT" build/pixie.elf + elif [ "$(uname -s)" = "Linux" ] && command -v docker >/dev/null 2>&1 \ + && docker info >/dev/null 2>&1; then + # Docker --device passthrough only works on Linux; on macOS Docker + # Desktop runs in a VM and host /dev/tty paths aren't visible. + IDF_IMAGE="${IDF_IMAGE:-espressif/idf:v5.5.4}" + exec docker run --rm -it \ + -v "$PWD":/project -w /project -e HOME=/tmp \ + --device "$PORT" \ + "$IDF_IMAGE" idf.py -p "$PORT" monitor + elif command -v screen >/dev/null 2>&1; then + echo "note: 'screen' won't symbolicate panic addresses; install" >&2 + echo " esp-idf-monitor for that: pip install esp-idf-monitor" >&2 + exec screen "$PORT" 115200 + else + echo "warning: no monitor tool available" >&2 + echo " install one: pip install esp-idf-monitor" >&2 + fi +else + echo "Monitor with: ./flash.sh --monitor (or) screen $PORT 115200" +fi diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f7a69a0..ab1154a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,10 +1,14 @@ idf_component_register( SRCS "main.c" + "panel-bytes.c" "panel-connect.c" + "panel-cyber.c" "panel-gifs.c" + "panel-life.c" "panel-menu.c" "panel-space.c" + "panel-stats.c" "panel-tx.c" "utils.c" diff --git a/main/main.c b/main/main.c index a07c39f..96f1e8e 100644 --- a/main/main.c +++ b/main/main.c @@ -23,6 +23,8 @@ #define GIT_COMMIT ("unknown") #endif +#define PIXIE_FW_VERSION FFX_VERSION(0, 2, 0) + static int initPanel(void *arg) { return pushPanelMenu(); @@ -33,7 +35,7 @@ void app_main() { FFX_LOG("GIT Commit: %s", GIT_COMMIT); - ffx_init(ffx_demo_backgroundPixies, initPanel, NULL); + ffx_init(PIXIE_FW_VERSION, ffx_demo_backgroundPixies, initPanel, NULL); while (1) { ffx_dumpStats(); diff --git a/main/panel-bytes.c b/main/panel-bytes.c new file mode 100644 index 0000000..56c0960 --- /dev/null +++ b/main/panel-bytes.c @@ -0,0 +1,149 @@ +#include +#include + +#include "firefly-hollows.h" +#include "firefly-scene.h" + +#include "panels.h" +#include "utils.h" + + +#define COL_COUNT (12) +#define TRAIL_LEN (10) +#define DOT_SIZE (10) +#define COL_STRIDE (20) +#define COL_OFFSET ((240 - (COL_COUNT * COL_STRIDE)) / 2) +#define DOT_STRIDE (16) + +#define TOTAL_RANGE (240 + TRAIL_LEN * DOT_STRIDE) + + +typedef struct ByteCol { + uint16_t period; + uint16_t phase; +} ByteCol; + +typedef struct BytesState { + FfxScene scene; + FfxNode dots[COL_COUNT * TRAIL_LEN]; + FfxNode glyphs[COL_COUNT]; + ByteCol cols[COL_COUNT]; + uint32_t rngState; +} BytesState; + + +static uint32_t rngNext(uint32_t *s) { + uint32_t x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *s = x ? x : 0xC0FFEE42; + return *s; +} + +static void onRender(FfxEvent event, FfxEventProps props, void *_state) { + BytesState *state = _state; + uint32_t t = ticks(); + + for (int c = 0; c < COL_COUNT; c++) { + ByteCol *col = &state->cols[c]; + int32_t headY = (int32_t)(((t + col->phase) / col->period) % TOTAL_RANGE); + int32_t x = COL_OFFSET + c * COL_STRIDE; + + for (int i = 0; i < TRAIL_LEN; i++) { + int32_t y = headY - i * DOT_STRIDE; + FfxNode dot = state->dots[c * TRAIL_LEN + i]; + if (y < -DOT_SIZE || y > 240) { + ffx_sceneNode_setHidden(dot, true); + continue; + } + ffx_sceneNode_setHidden(dot, false); + ffx_sceneNode_setPosition(dot, ffx_point(x, y)); + } + + FfxNode glyph = state->glyphs[c]; + int32_t gy = headY - 5; + if (gy < 0 || gy > 240) { + ffx_sceneNode_setHidden(glyph, true); + } else { + ffx_sceneNode_setHidden(glyph, false); + ffx_sceneNode_setPosition(glyph, ffx_point(x + DOT_SIZE / 2, gy)); + } + } +} + +static void onKeys(FfxEvent event, FfxEventProps props, void *_state) { + if (props.keys.down & FfxKeyCancel) { + ffx_popPanel(0); + } +} + +static int initFunc(FfxScene scene, FfxNode panel, void *_state, void *arg) { + BytesState *state = _state; + state->scene = scene; + state->rngState = 0x13371337 ^ ticks(); + + FfxNode bg = ffx_scene_createBox(scene, ffx_size(240, 240)); + ffx_sceneBox_setColor(bg, COLOR_BLACK); + ffx_sceneGroup_appendChild(panel, bg); + ffx_sceneNode_setPosition(bg, ffx_point(0, 0)); + + static const char *glyphChars[8] = { "0", "1", "X", "F", "A", "Z", "7", "$" }; + + for (int c = 0; c < COL_COUNT; c++) { + ByteCol *col = &state->cols[c]; + col->period = 6 + (rngNext(&state->rngState) % 12); + col->phase = rngNext(&state->rngState) % 4096; + + for (int i = 0; i < TRAIL_LEN; i++) { + FfxNode dot = ffx_scene_createBox(scene, + ffx_size(DOT_SIZE, DOT_SIZE)); + uint8_t opacity; + color_ffxt color; + if (i == 0) { + color = ffx_color_rgb(220, 255, 220); + opacity = MAX_OPACITY; + } else { + color = ffx_color_rgb(0, 255, 65); + int fade = MAX_OPACITY - (i * MAX_OPACITY / TRAIL_LEN); + if (fade < 2) { fade = 2; } + opacity = (uint8_t)fade; + } + ffx_sceneBox_setColor(dot, color); + ffx_sceneBox_setOpacity(dot, opacity); + ffx_sceneGroup_appendChild(panel, dot); + ffx_sceneNode_setPosition(dot, ffx_point(0, -32)); + state->dots[c * TRAIL_LEN + i] = dot; + } + + const char *glyph = glyphChars[rngNext(&state->rngState) & 7]; + FfxNode g = ffx_scene_createLabel(scene, FfxFontMedium, glyph); + ffx_sceneGroup_appendChild(panel, g); + ffx_sceneNode_setPosition(g, ffx_point(0, -32)); + ffx_sceneLabel_setAlign(g, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(g, COLOR_BLACK); + state->glyphs[c] = g; + } + + FfxNode title = ffx_scene_createLabel(scene, FfxFontLargeBold, + "BYTE//STREAM"); + ffx_sceneGroup_appendChild(panel, title); + ffx_sceneNode_setPosition(title, ffx_point(120, 18)); + ffx_sceneLabel_setAlign(title, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(title, COLOR_BLACK); + + FfxNode hint = ffx_scene_createLabel(scene, FfxFontMedium, "[CANCEL] EXIT"); + ffx_sceneGroup_appendChild(panel, hint); + ffx_sceneNode_setPosition(hint, ffx_point(120, 228)); + ffx_sceneLabel_setAlign(hint, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(hint, COLOR_BLACK); + + ffx_onEvent(FfxEventKeys, onKeys, state); + ffx_onEvent(FfxEventRenderScene, onRender, state); + + return 0; +} + +int pushPanelBytes() { + return ffx_pushPanel(initFunc, sizeof(BytesState), NULL); +} diff --git a/main/panel-cyber.c b/main/panel-cyber.c new file mode 100644 index 0000000..34d33b2 --- /dev/null +++ b/main/panel-cyber.c @@ -0,0 +1,154 @@ +#include +#include + +#include "firefly-hollows.h" +#include "firefly-scene.h" + +#include "panels.h" +#include "utils.h" + + +#define BAR_COUNT (8) +#define BAR_WIDTH (22) +#define BAR_GAP (4) +#define BAR_AREA_LEFT (16) +#define BAR_FLOOR_Y (220) +#define BAR_HEIGHT_MAX (150) + +#define SCAN_SPEED_MS (12) + + +typedef struct CyberState { + FfxScene scene; + FfxNode bars[BAR_COUNT]; + FfxNode floor; + FfxNode scanline; + FfxNode tickerDot; +} CyberState; + + +static const uint16_t barPeriods[BAR_COUNT] = { + 1100, 870, 1450, 720, 1230, 980, 1610, 830 +}; + +static const uint16_t barPhases[BAR_COUNT] = { + 0, 200, 450, 730, 1000, 1300, 1500, 1850 +}; + + +static uint32_t triangleWave(uint32_t t, uint32_t period, uint32_t phase, + uint32_t maxVal) { + uint32_t pos = (t + phase) % period; + uint32_t half = period / 2; + if (pos < half) { return (pos * maxVal) / half; } + return maxVal - ((pos - half) * maxVal) / half; +} + +static void neonBarColor(int idx, uint8_t *r, uint8_t *g, uint8_t *b) { + static const uint8_t palette[BAR_COUNT][3] = { + { 0, 255, 65 }, // matrix green + { 0, 255, 255 }, // cyan + { 255, 0, 200 }, // hot pink + { 255, 220, 0 }, // amber + { 120, 0, 255 }, // violet + { 255, 80, 0 }, // neon orange + { 0, 140, 255 }, // electric blue + { 255, 0, 80 }, // blood red + }; + *r = palette[idx][0]; + *g = palette[idx][1]; + *b = palette[idx][2]; +} + +static void onRender(FfxEvent event, FfxEventProps props, void *_state) { + CyberState *state = _state; + uint32_t t = ticks(); + + for (int i = 0; i < BAR_COUNT; i++) { + uint32_t h = triangleWave(t, barPeriods[i], barPhases[i], + BAR_HEIGHT_MAX); + int32_t x = BAR_AREA_LEFT + i * (BAR_WIDTH + BAR_GAP); + int32_t y = BAR_FLOOR_Y - h; + ffx_sceneNode_setPosition(state->bars[i], ffx_point(x, y)); + } + + int32_t sy = (t / SCAN_SPEED_MS) % 240; + ffx_sceneNode_setPosition(state->scanline, ffx_point(0, sy)); + + int32_t tx = (t / 20) % 240; + ffx_sceneNode_setPosition(state->tickerDot, ffx_point(tx, 218)); +} + +static void onKeys(FfxEvent event, FfxEventProps props, void *_state) { + if (props.keys.down & FfxKeyCancel) { + ffx_popPanel(0); + } +} + +static int initFunc(FfxScene scene, FfxNode panel, void *_state, void *arg) { + CyberState *state = _state; + state->scene = scene; + + FfxNode bg = ffx_scene_createBox(scene, ffx_size(240, 240)); + ffx_sceneBox_setColor(bg, ffx_color_rgb(8, 0, 16)); + ffx_sceneGroup_appendChild(panel, bg); + ffx_sceneNode_setPosition(bg, ffx_point(0, 0)); + + FfxNode title = ffx_scene_createLabel(scene, FfxFontLargeBold, + "CYBER//PULSE"); + ffx_sceneGroup_appendChild(panel, title); + ffx_sceneNode_setPosition(title, ffx_point(120, 22)); + ffx_sceneLabel_setAlign(title, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(title, ffx_color_rgb(0, 0, 0)); + + FfxNode subtitle = ffx_scene_createLabel(scene, FfxFontMedium, + "BPM 128 // SYNC OK"); + ffx_sceneGroup_appendChild(panel, subtitle); + ffx_sceneNode_setPosition(subtitle, ffx_point(120, 42)); + ffx_sceneLabel_setAlign(subtitle, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(subtitle, ffx_color_rgb(0, 0, 0)); + + FfxNode floor = ffx_scene_createBox(scene, ffx_size(240, 2)); + ffx_sceneBox_setColor(floor, ffx_color_rgb(0, 255, 65)); + ffx_sceneGroup_appendChild(panel, floor); + ffx_sceneNode_setPosition(floor, ffx_point(0, BAR_FLOOR_Y)); + state->floor = floor; + + for (int i = 0; i < BAR_COUNT; i++) { + FfxNode bar = ffx_scene_createBox(scene, + ffx_size(BAR_WIDTH, BAR_HEIGHT_MAX)); + uint8_t r, g, b; + neonBarColor(i, &r, &g, &b); + ffx_sceneBox_setColor(bar, ffx_color_rgb(r, g, b)); + ffx_sceneGroup_appendChild(panel, bar); + int32_t x = BAR_AREA_LEFT + i * (BAR_WIDTH + BAR_GAP); + ffx_sceneNode_setPosition(bar, ffx_point(x, BAR_FLOOR_Y)); + state->bars[i] = bar; + } + + FfxNode scanline = ffx_scene_createBox(scene, ffx_size(240, 2)); + ffx_sceneBox_setColor(scanline, ffx_color_rgb(180, 255, 220)); + ffx_sceneBox_setOpacity(scanline, 8); + ffx_sceneGroup_appendChild(panel, scanline); + state->scanline = scanline; + + FfxNode tickerDot = ffx_scene_createBox(scene, ffx_size(3, 3)); + ffx_sceneBox_setColor(tickerDot, ffx_color_rgb(0, 255, 255)); + ffx_sceneGroup_appendChild(panel, tickerDot); + state->tickerDot = tickerDot; + + FfxNode hint = ffx_scene_createLabel(scene, FfxFontMedium, "[CANCEL] EXIT"); + ffx_sceneGroup_appendChild(panel, hint); + ffx_sceneNode_setPosition(hint, ffx_point(120, 232)); + ffx_sceneLabel_setAlign(hint, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(hint, ffx_color_rgb(0, 0, 0)); + + ffx_onEvent(FfxEventKeys, onKeys, state); + ffx_onEvent(FfxEventRenderScene, onRender, state); + + return 0; +} + +int pushPanelCyber() { + return ffx_pushPanel(initFunc, sizeof(CyberState), NULL); +} diff --git a/main/panel-life.c b/main/panel-life.c new file mode 100644 index 0000000..49756a1 --- /dev/null +++ b/main/panel-life.c @@ -0,0 +1,251 @@ +#include +#include +#include + +#include "firefly-hollows.h" +#include "firefly-scene.h" + +#include "panels.h" +#include "utils.h" + + +#define GRID_DIM (16) +#define CELL_SIZE (14) +#define CELL_GAP (1) +#define GRID_PIXELS (GRID_DIM * (CELL_SIZE + CELL_GAP)) +#define GRID_OFFSET ((240 - GRID_PIXELS) / 2) +#define GRID_TOP (32) + +#define STEP_INTERVAL (160) + +#define PRESET_COUNT (4) + + +typedef struct LifeState { + FfxScene scene; + FfxNode cells[GRID_DIM * GRID_DIM]; + FfxNode presetLabels[PRESET_COUNT]; + + uint8_t board[GRID_DIM * GRID_DIM]; + uint8_t scratch[GRID_DIM * GRID_DIM]; + + int preset; + bool paused; + uint32_t nextStepAt; + uint32_t rngState; +} LifeState; + + +static const char *presetNames[PRESET_COUNT] = { + "GLIDER", "PULSAR", "RPENTOMINO", "RANDOM" +}; + +static uint32_t rngNext(uint32_t *s) { + uint32_t x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *s = x ? x : 0xCAFEBABE; + return *s; +} + +static void boardClear(LifeState *life) { + memset(life->board, 0, sizeof(life->board)); +} + +static void boardSet(LifeState *life, int x, int y, uint8_t v) { + if (x < 0 || x >= GRID_DIM || y < 0 || y >= GRID_DIM) { return; } + life->board[y * GRID_DIM + x] = v ? 1 : 0; +} + +static void seedGlider(LifeState *life) { + boardClear(life); + boardSet(life, 1, 0, 1); + boardSet(life, 2, 1, 1); + boardSet(life, 0, 2, 1); + boardSet(life, 1, 2, 1); + boardSet(life, 2, 2, 1); +} + +static void seedPulsar(LifeState *life) { + static const int8_t cells[][2] = { + {2,0},{3,0},{4,0},{8,0},{9,0},{10,0}, + {0,2},{5,2},{7,2},{12,2}, + {0,3},{5,3},{7,3},{12,3}, + {0,4},{5,4},{7,4},{12,4}, + {2,5},{3,5},{4,5},{8,5},{9,5},{10,5}, + {2,7},{3,7},{4,7},{8,7},{9,7},{10,7}, + {0,8},{5,8},{7,8},{12,8}, + {0,9},{5,9},{7,9},{12,9}, + {0,10},{5,10},{7,10},{12,10}, + {2,12},{3,12},{4,12},{8,12},{9,12},{10,12} + }; + boardClear(life); + int ox = (GRID_DIM - 13) / 2; + int oy = (GRID_DIM - 13) / 2; + for (size_t i = 0; i < sizeof(cells) / sizeof(cells[0]); i++) { + boardSet(life, ox + cells[i][0], oy + cells[i][1], 1); + } +} + +static void seedRPentomino(LifeState *life) { + boardClear(life); + int cx = GRID_DIM / 2; + int cy = GRID_DIM / 2; + boardSet(life, cx, cy - 1, 1); + boardSet(life, cx + 1, cy - 1, 1); + boardSet(life, cx - 1, cy, 1); + boardSet(life, cx, cy, 1); + boardSet(life, cx, cy + 1, 1); +} + +static void seedRandom(LifeState *life) { + boardClear(life); + for (int i = 0; i < GRID_DIM * GRID_DIM; i++) { + life->board[i] = (rngNext(&life->rngState) & 0x3) == 0 ? 1 : 0; + } +} + +static void applyPreset(LifeState *life) { + switch (life->preset) { + case 0: seedGlider(life); break; + case 1: seedPulsar(life); break; + case 2: seedRPentomino(life); break; + default: seedRandom(life); break; + } +} + +static void stepBoard(LifeState *life) { + for (int y = 0; y < GRID_DIM; y++) { + for (int x = 0; x < GRID_DIM; x++) { + int n = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) { continue; } + int nx = (x + dx + GRID_DIM) % GRID_DIM; + int ny = (y + dy + GRID_DIM) % GRID_DIM; + n += life->board[ny * GRID_DIM + nx]; + } + } + uint8_t alive = life->board[y * GRID_DIM + x]; + life->scratch[y * GRID_DIM + x] = + (alive && (n == 2 || n == 3)) || (!alive && n == 3); + } + } + memcpy(life->board, life->scratch, sizeof(life->board)); +} + +static void renderBoard(LifeState *life) { + for (int i = 0; i < GRID_DIM * GRID_DIM; i++) { + ffx_sceneNode_setHidden(life->cells[i], !life->board[i]); + } +} + +static void updatePresetHighlight(LifeState *life) { + for (int i = 0; i < PRESET_COUNT; i++) { + ffx_sceneNode_setHidden(life->presetLabels[i], i != life->preset); + } +} + +static void onRender(FfxEvent event, FfxEventProps props, void *_state) { + LifeState *life = _state; + if (life->paused) { return; } + + uint32_t t = ticks(); + if (t < life->nextStepAt) { return; } + life->nextStepAt = t + STEP_INTERVAL; + + stepBoard(life); + renderBoard(life); +} + +static void onKeys(FfxEvent event, FfxEventProps props, void *_state) { + LifeState *life = _state; + + switch (props.keys.down) { + case FfxKeyCancel: + ffx_popPanel(0); + break; + case FfxKeyOk: + life->paused = !life->paused; + life->nextStepAt = ticks(); + break; + case FfxKeyNorth: + life->preset = (life->preset + PRESET_COUNT - 1) % PRESET_COUNT; + applyPreset(life); + renderBoard(life); + updatePresetHighlight(life); + break; + case FfxKeySouth: + life->preset = (life->preset + 1) % PRESET_COUNT; + applyPreset(life); + renderBoard(life); + updatePresetHighlight(life); + break; + } +} + +static int initFunc(FfxScene scene, FfxNode panel, void *_state, void *arg) { + LifeState *life = _state; + life->scene = scene; + life->preset = 0; + life->paused = false; + life->rngState = 0xDEADBEEF ^ ticks(); + + FfxNode bg = ffx_scene_createBox(scene, ffx_size(240, 240)); + ffx_sceneBox_setColor(bg, ffx_color_rgb(2, 6, 4)); + ffx_sceneGroup_appendChild(panel, bg); + ffx_sceneNode_setPosition(bg, ffx_point(0, 0)); + + FfxNode title = ffx_scene_createLabel(scene, FfxFontLargeBold, "LIFE//GRID"); + ffx_sceneGroup_appendChild(panel, title); + ffx_sceneNode_setPosition(title, ffx_point(8, 16)); + ffx_sceneLabel_setAlign(title, FfxTextAlignLeft | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(title, ffx_color_rgb(0, 0, 0)); + + for (int i = 0; i < PRESET_COUNT; i++) { + FfxNode label = ffx_scene_createLabel(scene, FfxFontMedium, + presetNames[i]); + ffx_sceneGroup_appendChild(panel, label); + ffx_sceneNode_setPosition(label, ffx_point(232, 16)); + ffx_sceneLabel_setAlign(label, FfxTextAlignRight | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(label, ffx_color_rgb(0, 0, 0)); + life->presetLabels[i] = label; + } + updatePresetHighlight(life); + + for (int y = 0; y < GRID_DIM; y++) { + for (int x = 0; x < GRID_DIM; x++) { + FfxNode cell = ffx_scene_createBox(scene, + ffx_size(CELL_SIZE, CELL_SIZE)); + uint8_t g = 120 + ((x + y) * 8) % 130; + ffx_sceneBox_setColor(cell, ffx_color_rgb(0, g, 65)); + ffx_sceneGroup_appendChild(panel, cell); + ffx_sceneNode_setPosition(cell, ffx_point( + GRID_OFFSET + x * (CELL_SIZE + CELL_GAP), + GRID_TOP + y * (CELL_SIZE + CELL_GAP))); + ffx_sceneNode_setHidden(cell, true); + life->cells[y * GRID_DIM + x] = cell; + } + } + + FfxNode hint = ffx_scene_createLabel(scene, FfxFontMedium, + "N/S:SEED OK:PAUSE X:EXIT"); + ffx_sceneGroup_appendChild(panel, hint); + ffx_sceneNode_setPosition(hint, ffx_point(120, 230)); + ffx_sceneLabel_setAlign(hint, FfxTextAlignCenter | FfxTextAlignMiddle); + ffx_sceneLabel_setOutlineColor(hint, ffx_color_rgb(0, 0, 0)); + + applyPreset(life); + renderBoard(life); + life->nextStepAt = ticks() + STEP_INTERVAL; + + ffx_onEvent(FfxEventKeys, onKeys, life); + ffx_onEvent(FfxEventRenderScene, onRender, life); + + return 0; +} + +int pushPanelLife() { + return ffx_pushPanel(initFunc, sizeof(LifeState), NULL); +} diff --git a/main/panel-menu.c b/main/panel-menu.c index dd2a12e..c8d20e2 100644 --- a/main/panel-menu.c +++ b/main/panel-menu.c @@ -9,82 +9,144 @@ #include "images/image-arrow.h" +#define ITEM_COUNT (7) +#define VISIBLE_ROWS (3) +#define ROW_HEIGHT (40) +#define ROW_FIRST_Y (63) +#define ARROW_X (25) +#define ARROW_OFFSET_Y (-5) +#define LABEL_X (70) + + typedef struct State { - size_t cursor; + int cursor; + int scrollOffset; FfxScene scene; - FfxNode nodeCursor; + FfxNode arrow; + FfxNode labels[ITEM_COUNT]; } State; +static const char *menuItems[ITEM_COUNT] = { + "Wallet", + "GIFs", + "Le Space", + "Cyber Pulse", + "Life Grid", + "Byte Stream", + "Sys Stats", +}; + + +static void launchItem(int idx) { + switch (idx) { + case 0: pushPanelConnect(); break; + case 1: pushPanelGifs(); break; + case 2: pushPanelSpace(); break; + case 3: pushPanelCyber(); break; + case 4: pushPanelLife(); break; + case 5: pushPanelBytes(); break; + case 6: pushPanelStats(); break; + } +} + +static int rowYForSlot(int slot) { + return ROW_FIRST_Y + slot * ROW_HEIGHT; +} + +static void layoutItems(State *app) { + for (int i = 0; i < ITEM_COUNT; i++) { + int slot = i - app->scrollOffset; + if (slot < 0 || slot >= VISIBLE_ROWS) { + ffx_sceneNode_setHidden(app->labels[i], true); + continue; + } + ffx_sceneNode_setHidden(app->labels[i], false); + ffx_sceneNode_setPosition(app->labels[i], + ffx_point(LABEL_X, rowYForSlot(slot))); + } +} + +static void scrollToCursor(State *app) { + if (app->cursor < app->scrollOffset) { + app->scrollOffset = app->cursor; + } else if (app->cursor >= app->scrollOffset + VISIBLE_ROWS) { + app->scrollOffset = app->cursor - (VISIBLE_ROWS - 1); + } +} + +static void moveArrow(State *app, bool animated) { + int slot = app->cursor - app->scrollOffset; + int y = rowYForSlot(slot) + ARROW_OFFSET_Y; + ffx_sceneNode_stopAnimations(app->arrow, FfxSceneActionStopCurrent); + if (animated) { + ffx_sceneNode_animatePosition(app->arrow, ffx_point(ARROW_X, y), + 0, 150, FfxCurveEaseOutQuad, NULL, NULL); + } else { + ffx_sceneNode_setPosition(app->arrow, ffx_point(ARROW_X, y)); + } +} + static void onKeys(FfxEvent event, FfxEventProps props, void *_app) { State *app = _app; - - switch(props.keys.down) { - case FfxKeyOk: { - uint32_t result = 0; - switch(app->cursor) { - case 0: - result = pushPanelConnect(); - break; - case 1: -// result = pushPanelGifs(NULL); - break; - case 2: - result = pushPanelSpace(NULL); - break; - } - printf("RESULT-men: %ld\n", result); + switch (props.keys.down) { + case FfxKeyOk: + launchItem(app->cursor); return; - } case FfxKeyNorth: if (app->cursor == 0) { return; } app->cursor--; break; case FfxKeySouth: - if (app->cursor == 2) { return; } + if (app->cursor == ITEM_COUNT - 1) { return; } app->cursor++; break; default: return; } - - ffx_sceneNode_stopAnimations(app->nodeCursor, FfxSceneActionStopCurrent); - ffx_sceneNode_animatePosition(app->nodeCursor, - ffx_point(25, 58 + (app->cursor * 40)), 0, 150, - FfxCurveEaseOutQuad, NULL, NULL); + scrollToCursor(app); + layoutItems(app); + moveArrow(app, true); } static int initFunc(FfxScene scene, FfxNode node, void *_app, void *arg) { State *app = _app; app->scene = scene; + app->cursor = 0; + app->scrollOffset = 0; FfxNode box = ffx_scene_createBox(scene, ffx_size(200, 180)); ffx_sceneBox_setColor(box, RGBA_DARKER75); ffx_sceneGroup_appendChild(node, box); - ffx_sceneNode_setPosition(box, (FfxPoint){ .x = 20, .y = 30 }); - - FfxNode text; - - text = ffx_scene_createLabel(scene, FfxFontLarge, "Wallet"); - ffx_sceneGroup_appendChild(node, text); - ffx_sceneNode_setPosition(text, (FfxPoint){ .x = 70, .y = 63 }); - - text = ffx_scene_createLabel(scene, FfxFontLarge, "GIFs"); - ffx_sceneGroup_appendChild(node, text); - ffx_sceneNode_setPosition(text, (FfxPoint){ .x = 70, .y = 103 }); - - text = ffx_scene_createLabel(scene, FfxFontLarge, "Le Space"); - ffx_sceneGroup_appendChild(node, text); - ffx_sceneNode_setPosition(text, (FfxPoint){ .x = 70, .y = 143 }); + ffx_sceneNode_setPosition(box, ffx_point(20, 30)); + + FfxNode topRule = ffx_scene_createBox(scene, ffx_size(200, 1)); + ffx_sceneBox_setColor(topRule, ffx_color_rgb(0, 255, 65)); + ffx_sceneGroup_appendChild(node, topRule); + ffx_sceneNode_setPosition(topRule, ffx_point(20, 30)); + + FfxNode bottomRule = ffx_scene_createBox(scene, ffx_size(200, 1)); + ffx_sceneBox_setColor(bottomRule, ffx_color_rgb(0, 255, 65)); + ffx_sceneGroup_appendChild(node, bottomRule); + ffx_sceneNode_setPosition(bottomRule, ffx_point(20, 209)); + + for (int i = 0; i < ITEM_COUNT; i++) { + FfxNode label = ffx_scene_createLabel(scene, FfxFontLarge, + menuItems[i]); + ffx_sceneGroup_appendChild(node, label); + ffx_sceneNode_setHidden(label, true); + app->labels[i] = label; + } FfxNode cursor = ffx_scene_createImage(scene, image_arrow, sizeof(image_arrow)); ffx_sceneGroup_appendChild(node, cursor); - ffx_sceneNode_setPosition(cursor, (FfxPoint){ .x = 25, .y = 58 }); + app->arrow = cursor; - app->nodeCursor = cursor; + layoutItems(app); + moveArrow(app, false); ffx_onEvent(FfxEventKeys, onKeys, app); diff --git a/main/panel-stats.c b/main/panel-stats.c new file mode 100644 index 0000000..005c2c1 --- /dev/null +++ b/main/panel-stats.c @@ -0,0 +1,184 @@ +#include +#include + +#include "esp_chip_info.h" +#include "esp_idf_version.h" +#include "esp_system.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "firefly-hollows.h" +#include "firefly-scene.h" + +#include "panels.h" +#include "utils.h" + + +#ifndef GIT_COMMIT +#define GIT_COMMIT ("unknown") +#endif + + +#define HISTORY_LEN (60) +#define BAR_AREA_LEFT (20) +#define BAR_AREA_TOP (170) +#define BAR_HEIGHT_MAX (50) + + +typedef struct StatsState { + FfxScene scene; + + FfxNode heapHistory[HISTORY_LEN]; + FfxNode heapValueLabel; + FfxNode uptimeValueLabel; + FfxNode fpsValueLabel; + + uint32_t bootTime; + uint32_t nextSampleAt; + int historyHead; + uint32_t initialFreeHeap; + + uint32_t fpsWindowStart; + uint32_t fpsFrames; +} StatsState; + + +static FfxNode addLabel(FfxScene scene, FfxNode parent, const char *text, + int32_t x, int32_t y, FfxFont font, uint32_t align) { + FfxNode label = ffx_scene_createLabel(scene, font, text); + ffx_sceneGroup_appendChild(parent, label); + ffx_sceneNode_setPosition(label, ffx_point(x, y)); + ffx_sceneLabel_setAlign(label, align); + ffx_sceneLabel_setOutlineColor(label, COLOR_BLACK); + return label; +} + +static void renderHeapBar(StatsState *state, int slot, uint32_t freeBytes) { + if (state->initialFreeHeap == 0) { + state->initialFreeHeap = freeBytes; + } + uint32_t scale = state->initialFreeHeap; + if (scale < freeBytes) { scale = freeBytes; } + if (scale == 0) { scale = 1; } + + int32_t h = (int32_t)((freeBytes * BAR_HEIGHT_MAX) / scale); + if (h < 1) { h = 1; } + if (h > BAR_HEIGHT_MAX) { h = BAR_HEIGHT_MAX; } + + int32_t x = BAR_AREA_LEFT + slot * 3; + int32_t y = BAR_AREA_TOP + (BAR_HEIGHT_MAX - h); + ffx_sceneNode_setPosition(state->heapHistory[slot], ffx_point(x, y)); +} + +static void onRender(FfxEvent event, FfxEventProps props, void *_state) { + StatsState *state = _state; + + state->fpsFrames++; + + uint32_t t = ticks(); + if (t < state->nextSampleAt) { return; } + state->nextSampleAt = t + 250; + + uint32_t freeHeap = esp_get_free_heap_size(); + renderHeapBar(state, state->historyHead, freeHeap); + state->historyHead = (state->historyHead + 1) % HISTORY_LEN; + + ffx_sceneLabel_setTextFormat(state->heapValueLabel, "%lu KB", + (unsigned long)(freeHeap / 1024)); + + uint32_t uptimeMs = t - state->bootTime; + uint32_t secs = uptimeMs / 1000; + ffx_sceneLabel_setTextFormat(state->uptimeValueLabel, "%02lu:%02lu:%02lu", + (unsigned long)(secs / 3600), + (unsigned long)((secs / 60) % 60), + (unsigned long)(secs % 60)); + + uint32_t windowMs = t - state->fpsWindowStart; + if (windowMs >= 1000) { + uint32_t fps = (state->fpsFrames * 1000) / windowMs; + ffx_sceneLabel_setTextFormat(state->fpsValueLabel, "%lu FPS", + (unsigned long)fps); + state->fpsWindowStart = t; + state->fpsFrames = 0; + } +} + +static void onKeys(FfxEvent event, FfxEventProps props, void *_state) { + if (props.keys.down & FfxKeyCancel) { + ffx_popPanel(0); + } +} + +static int initFunc(FfxScene scene, FfxNode panel, void *_state, void *arg) { + StatsState *state = _state; + state->scene = scene; + state->bootTime = ticks(); + state->fpsWindowStart = state->bootTime; + + FfxNode bg = ffx_scene_createBox(scene, ffx_size(240, 240)); + ffx_sceneBox_setColor(bg, ffx_color_rgb(0, 8, 12)); + ffx_sceneGroup_appendChild(panel, bg); + ffx_sceneNode_setPosition(bg, ffx_point(0, 0)); + + addLabel(scene, panel, "SYS//STATS", 120, 16, FfxFontLargeBold, + FfxTextAlignCenter | FfxTextAlignMiddle); + + addLabel(scene, panel, "CHIP", 14, 50, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + esp_chip_info_t chip; + esp_chip_info(&chip); + static char chipBuf[32]; + snprintf(chipBuf, sizeof(chipBuf), "ESP32-C3 r%d %dC", + (int)chip.revision, (int)chip.cores); + addLabel(scene, panel, chipBuf, 226, 50, FfxFontMedium, + FfxTextAlignRight | FfxTextAlignMiddle); + + addLabel(scene, panel, "IDF", 14, 70, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + addLabel(scene, panel, esp_get_idf_version(), 226, 70, FfxFontMedium, + FfxTextAlignRight | FfxTextAlignMiddle); + + addLabel(scene, panel, "BUILD", 14, 90, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + addLabel(scene, panel, GIT_COMMIT, 226, 90, FfxFontMedium, + FfxTextAlignRight | FfxTextAlignMiddle); + + addLabel(scene, panel, "UPTIME", 14, 110, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + state->uptimeValueLabel = addLabel(scene, panel, "00:00:00", 226, 110, + FfxFontMedium, FfxTextAlignRight | FfxTextAlignMiddle); + + addLabel(scene, panel, "HEAP", 14, 130, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + state->heapValueLabel = addLabel(scene, panel, "-- KB", 226, 130, + FfxFontMedium, FfxTextAlignRight | FfxTextAlignMiddle); + + addLabel(scene, panel, "RENDER", 14, 150, FfxFontMedium, + FfxTextAlignLeft | FfxTextAlignMiddle); + state->fpsValueLabel = addLabel(scene, panel, "-- FPS", 226, 150, + FfxFontMedium, FfxTextAlignRight | FfxTextAlignMiddle); + + for (int i = 0; i < HISTORY_LEN; i++) { + FfxNode bar = ffx_scene_createBox(scene, ffx_size(2, BAR_HEIGHT_MAX)); + ffx_sceneBox_setColor(bar, ffx_color_rgb(0, 200, 80)); + ffx_sceneGroup_appendChild(panel, bar); + ffx_sceneNode_setPosition(bar, + ffx_point(BAR_AREA_LEFT + i * 3, BAR_AREA_TOP + BAR_HEIGHT_MAX)); + state->heapHistory[i] = bar; + } + + addLabel(scene, panel, "[CANCEL] EXIT", 120, 232, FfxFontMedium, + FfxTextAlignCenter | FfxTextAlignMiddle); + + state->nextSampleAt = state->bootTime; + + ffx_onEvent(FfxEventKeys, onKeys, state); + ffx_onEvent(FfxEventRenderScene, onRender, state); + + return 0; +} + +int pushPanelStats() { + return ffx_pushPanel(initFunc, sizeof(StatsState), NULL); +} diff --git a/main/panel-tx.c b/main/panel-tx.c index 5e953c5..935bebd 100644 --- a/main/panel-tx.c +++ b/main/panel-tx.c @@ -122,7 +122,7 @@ static int initAddressFunc(void *info, void *_state, void *_arg) { } // Buttons - ffx_appendInfoButton(info, "BACK", COLOR_BACK, clickApprove, NULLARG); + ffx_appendInfoButton(info, "BACK", COLOR_NAVONLY, clickApprove, NULLARG); return 0; } @@ -234,7 +234,7 @@ static int initDataFunc(void *info, void *_state, void *arg) { } // Buttons - ffx_appendInfoButton(info, "BACK", COLOR_BACK, clickApprove, NULLARG); + ffx_appendInfoButton(info, "BACK", COLOR_NAVONLY, clickApprove, NULLARG); return true; } @@ -335,7 +335,7 @@ static int initNetworkFunc(void *info, void *_state, void *arg) { ffx_appendInfoEntry(info, "CHAIN ID", str, NULL, NULLARG); // Buttons - ffx_appendInfoButton(info, "BACK", COLOR_BACK, clickApprove, NULLARG); + ffx_appendInfoButton(info, "BACK", COLOR_NAVONLY, clickApprove, NULLARG); return 0; } diff --git a/main/panels.h b/main/panels.h index 884296b..920e77d 100644 --- a/main/panels.h +++ b/main/panels.h @@ -32,10 +32,6 @@ bool appendNetwork(void *info, FfxDataResult *chainId); int pushPanelMenu(); -//int pushPanelMenu(FfxInfoInitFunc initFunc, size_t stateSize, void *initArg); -//bool appendMenuItem(void *menu, const char* title, -// FfxInfoClickFunc clickFunc, FfxInfoClickArg clickArg); - /////////////////////////////// // Simple Game and Toy Panels @@ -47,12 +43,28 @@ typedef enum GameResult { } GameResult; // See: panel-gifs.c -void pushPanelGifs(); +int pushPanelGifs(); // See: panel-space.c GameResult pushPanelSpace(); +/////////////////////////////// +// Cyberdeck Demo Panels + +// See: panel-cyber.c +int pushPanelCyber(); + +// See: panel-life.c +int pushPanelLife(); + +// See: panel-bytes.c +int pushPanelBytes(); + +// See: panel-stats.c +int pushPanelStats(); + + /////////////////////////////// // Wallet Panels