diff --git a/Blink copy.png b/Blink copy.png new file mode 100644 index 0000000..b542dbf Binary files /dev/null and b/Blink copy.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ebd12e --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Panagotchi + +A Tamagotchi-style virtual pet for the **CH32V003F4P6** badge — a hacker-themed creature that evolves from `proc` → `user` → `root`, then either ascends to **Claude** or degrades into a **glitch** depending on how well you tend to it. + +Fits in ~12 KB of the 16 KB flash budget. + +## Hardware + +| Pin | Function | +|------|-----------------| +| PA1 | Indicator LED | +| PC0 | OK button | +| PC1/PC2 | SSD1306 128×64 OLED (I2C1) | +| PD2 | Passive piezo buzzer | +| PD5 | Right button | +| PD6 | Left button | + +Powered by a CR2032 coin cell via a TLV61070A boost converter to 3.3V. See [pinout.md](pinout.md) for the full pin assignment and power architecture. + +## Gameplay + +**Controls:** Left / Right move the menu cursor. OK activates the selected action. + +**Menu actions:** + +| Icon | Action | Effect | +|---------|---------|--------| +| Apple | FEED | +30 satiety, −5 hygiene | +| Ball | PLAY | +25 happy, −12 energy (requires energy ≥ 15) | +| Moon | SLEEP | Toggle nap; restores energy | +| Drop | CLEAN | Reset hygiene to 100, remove poop piles | +| Cross | HEAL | +35 health | +| Chart | STATS | Show stat bars + XP readout | +| Gear | SETTINGS| Screen timeout, brightness, sound | + +Stats decay on a ~15-second tick. Health responds to overall wellbeing — if any need drops below 15 it bleeds down; if all are above 50 it recovers. Let health reach 0 and the pet dies with a `SEGFAULT (11)`. + +**Evolution** (XP thresholds: 45 / 120 / 240): + +``` +proc → user → root → Claude (well-kept) / glitch (neglected) +``` + +Each stage gains a visual accessory: shades at `user`, a gaming headset and mic boom at `root`. + +**Species** (chosen on the welcome screen): + +- **Panko** — the panda; full 7-expression animation set +- **Shell** — a friendly CRT terminal with a `> _` prompt mouth +- **Tmux** — a split-pane terminal with a status bar + +## Building + +Requires [ch32v003fun](https://github.com/cnlohr/ch32v003fun) checked out as a sibling directory. + +```sh +# Generate sprites from source art +python3 sprites_gen.py > sprites.h + +# Build and flash +make +``` + +Flash via the `minichlink` or `wch-link` programmer. The Makefile targets `cv_flash` / `cv_clean` from ch32fun. + +Check real flash usage with: + +```sh +riscv-none-elf-size tama.elf +``` + +## Power saving + +The OLED is the biggest power draw. When the screen-off timeout fires, the display is blanked and the MCU enters WFI light-sleep. Two wake sources: + +- **Button press** (EXTI falling edge on PC0/PD5/PD6) — lights the screen back up +- **TIM2 overflow** (~once per game tick) — runs a background stat update and low-need chirp, without waking the display + +SRAM and all pet state survive WFI; only the CPU clock is gated. + +Brightness and screen-off timeout are adjustable in the settings menu. + +## Files + +| File | Purpose | +|------|---------| +| [tama.c](tama.c) | Main firmware: game logic, rendering, input, low-power | +| [sprites_gen.py](sprites_gen.py) | Python sprite generator — edit art here | +| [sprites.h](sprites.h) | Auto-generated 1-bpp sprite data (do not edit) | +| [funconfig.h](funconfig.h) | ch32fun config overrides (disables printf backends) | +| [pinout.md](pinout.md) | Full pin assignment and power architecture notes | +| [OPTIMIZATION.md](OPTIMIZATION.md) | Flash size backlog and optimization notes | diff --git a/custom_blink.png b/custom_blink.png new file mode 100644 index 0000000..b542dbf Binary files /dev/null and b/custom_blink.png differ diff --git a/custom_dead.png b/custom_dead.png new file mode 100644 index 0000000..48ca7a8 Binary files /dev/null and b/custom_dead.png differ diff --git a/custom_eat.png b/custom_eat.png new file mode 100644 index 0000000..e58581b Binary files /dev/null and b/custom_eat.png differ diff --git a/custom_happy.png b/custom_happy.png new file mode 100644 index 0000000..c8544cd Binary files /dev/null and b/custom_happy.png differ diff --git a/custom_idle.png b/custom_idle.png new file mode 100644 index 0000000..90a0d48 Binary files /dev/null and b/custom_idle.png differ diff --git a/custom_sad.png b/custom_sad.png new file mode 100644 index 0000000..c01e837 Binary files /dev/null and b/custom_sad.png differ diff --git a/custom_sleep.png b/custom_sleep.png new file mode 100644 index 0000000..8e64709 Binary files /dev/null and b/custom_sleep.png differ diff --git a/sprites.h b/sprites.h index 00acb26..6b93c81 100644 --- a/sprites.h +++ b/sprites.h @@ -9,108 +9,254 @@ // pana_idle: 32x32 static const unsigned char pana_idle[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFC, 0x3F, 0x80, - 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xef, 0xfb, 0xf0, + 0x0f, 0x83, 0xe0, 0xf8, + 0x0f, 0x93, 0xe4, 0xf8, + 0x0f, 0x11, 0xc4, 0x78, + 0x0f, 0x83, 0xe0, 0xf8, + 0x1f, 0x83, 0xe0, 0xfc, + 0x0f, 0xef, 0x7b, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x07, 0xf8, 0x0f, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; // pana_blink: 32x32 static const unsigned char pana_blink[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xC3, 0xC3, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFC, 0x3F, 0x80, - 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0x01, 0xc0, 0x78, + 0x0f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xfc, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x07, 0xf8, 0x0f, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; // pana_happy: 32x32 -static const unsigned char pana_happy[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xEF, 0xF7, 0xC0, 0x03, 0xE7, 0xEB, 0xC0, 0x03, 0xDB, 0xDB, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xF7, 0xEF, 0x80, - 0x00, 0xF7, 0xEF, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xFC, 0x3F, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, +static const unsigned char pana_happy[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xef, 0xfb, 0xf8, + 0x0f, 0xd7, 0xf5, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xfc, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xf8, 0x0f, 0xf8, + 0x0f, 0xfb, 0xef, 0xf8, + 0x0f, 0xf7, 0xf7, 0xf8, + 0x07, 0xfc, 0x1f, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; // pana_sad: 32x32 static const unsigned char pana_sad[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0x9F, 0xF9, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF, 0x80, - 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xF7, 0xEF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x0f, 0xef, 0xfb, 0xf8, + 0x0f, 0x83, 0xe0, 0xf8, + 0x0f, 0x93, 0xe4, 0xf8, + 0x0f, 0x11, 0xc4, 0x78, + 0x1f, 0x83, 0xe0, 0xfc, + 0x0f, 0x83, 0x60, 0xf8, + 0x0f, 0xec, 0x1b, 0xf8, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x07, 0xf7, 0xf7, 0xf0, + 0x07, 0xfb, 0xef, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; // pana_eat: 32x32 -static const unsigned char pana_eat[] ={ - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFC, 0x3F, 0x80, 0x01, 0xFD, 0x3F, 0x80, - 0x00, 0xFC, 0xBF, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x01, 0xFF, 0xFF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, -}; +static const unsigned char pana_eat[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xef, 0xfb, 0xf0, + 0x07, 0x83, 0xe0, 0xf0, + 0x0f, 0x93, 0xe4, 0xf8, + 0x0f, 0x11, 0xc4, 0x78, + 0x0f, 0x83, 0xe0, 0xf8, + 0x0f, 0x83, 0xe0, 0xf8, + 0x1f, 0xef, 0xfb, 0xfc, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x0f, 0xf8, 0x0f, 0xf8, + 0x07, 0xfc, 0x1f, 0xf0, + 0x07, 0xfc, 0x1f, 0xf0, + 0x03, 0xff, 0x7f, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, +}; // pana_sleep: 32x32 static const unsigned char pana_sleep[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0xF0, - 0x00, 0x31, 0x8C, 0x20, 0x00, 0x39, 0xDC, 0x40, 0x00, 0x3B, 0xDC, 0xF0, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xC3, 0xC3, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFE, 0x7F, 0x80, - 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0x01, 0xc0, 0x78, + 0x1f, 0xff, 0xff, 0xfc, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0x7f, 0xf8, + 0x0f, 0xfc, 0x1f, 0xf8, + 0x0f, 0xff, 0x7f, 0xf8, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; - // pana_dead: 32x32 static const unsigned char pana_dead[] = { - 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, - 0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, - 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, - 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, - 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, - 0x03, 0xDB, 0xDB, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, - 0x03, 0xDB, 0xDB, 0xC0, 0x01, 0xFD, 0xDF, 0x80, 0x01, 0xFA, 0xBF, 0x80, - 0x00, 0xF7, 0x7F, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, - 0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, - 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, - 0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x80, + 0x0f, 0xe0, 0x07, 0xf0, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x1f, 0xf0, 0x0f, 0xf8, + 0x3f, 0xf8, 0x9f, 0xfc, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x0f, 0xbb, 0xee, 0xf8, + 0x0f, 0xd7, 0xf5, 0xf8, + 0x0f, 0xef, 0xfb, 0xf8, + 0x0f, 0xd7, 0xf5, 0xf8, + 0x1f, 0xbb, 0xee, 0xfc, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xf8, + 0x0f, 0xf0, 0x07, 0xf8, + 0x07, 0xff, 0xff, 0xf0, + 0x07, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xe0, + 0x01, 0xff, 0xff, 0xc0, + 0x00, 0xff, 0xff, 0x80, + 0x00, 0x7f, 0xff, 0x00, + 0x00, 0x1f, 0xfc, 0x00, + 0x00, 0x00, 0x80, 0x00, }; enum { EXP_IDLE, EXP_BLINK, EXP_HAPPY, EXP_SAD, EXP_EAT, EXP_SLEEP, EXP_DEAD, EXP_COUNT }; @@ -406,17 +552,276 @@ static const unsigned char tmux_sleep[] = { 0x00, 0x00, 0x00, 0x00, }; -#define SPECIES_COUNT 3 -enum { SP_SHELL, SP_PANKO, SP_TMUX }; +// custom_idle: 32x32 +static const unsigned char custom_idle[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_blink: 32x32 +static const unsigned char custom_blink[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_happy: 32x32 +static const unsigned char custom_happy[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_sad: 32x32 +static const unsigned char custom_sad[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_eat: 32x32 +static const unsigned char custom_eat[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_sleep: 32x32 +static const unsigned char custom_sleep[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// custom_dead: 32x32 +static const unsigned char custom_dead[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +#define SPECIES_COUNT 4 +enum { SP_SHELL, SP_PANKO, SP_TMUX, SP_CUSTOM }; static const char *const species_names[SPECIES_COUNT] = { - "Shell", "Panko", "Tmux", + "Shell", "Panko", "Tmux", "Custom", }; static const unsigned char *const species_frames[SPECIES_COUNT][EXP_COUNT] = { { shell_idle, shell_idle, shell_happy, shell_sad, shell_happy, shell_sleep, shell_sad }, { pana_idle, pana_blink, pana_happy, pana_sad, pana_eat, pana_sleep, pana_dead }, { tmux_idle, tmux_idle, tmux_happy, tmux_sad, tmux_happy, tmux_sleep, tmux_sad }, + { custom_idle, custom_blink, custom_happy, custom_sad, custom_eat, custom_sleep, custom_dead }, }; +// Eye centre coords for custom species accessories (shades at user, headset at root). +// Pixel positions inside the 32x32 sprite -- adjust to match your art. +#define CUSTOM_EYE_CY 17 // eye centre row +#define CUSTOM_EYE_LX 11 // left eye centre column +#define CUSTOM_EYE_RX 21 // right eye centre column + // spr_grave: 32x32 static const unsigned char spr_grave[] = { 0x00, 0x00, 0x00, 0x00, @@ -677,12 +1082,32 @@ static const unsigned char icon_settings[] = { 0x03, 0xc0, }; -#define ICON_COUNT 7 +// icon_ir: 16x16 +static const unsigned char icon_ir[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x80, + 0x01, 0x40, + 0x02, 0x20, + 0x04, 0x10, + 0x00, 0x80, + 0x09, 0x48, + 0x02, 0x20, + 0x10, 0x84, + 0x00, 0x80, + 0x00, 0x80, + 0x00, 0x80, + 0x00, 0x80, + 0x01, 0xc0, + 0x00, 0x00, +}; + +#define ICON_COUNT 8 static const unsigned char *const menu_icons[ICON_COUNT] = { - icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings, + icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings, icon_ir, }; static const char *const menu_labels[ICON_COUNT] = { - "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS", + "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS", "IR BEAM", }; #endif // _SPRITES_H diff --git a/sprites_gen.py b/sprites_gen.py index 2b42150..cef9405 100644 --- a/sprites_gen.py +++ b/sprites_gen.py @@ -52,6 +52,45 @@ def from_art(art): g.append([1 if c in "#X1" else 0 for c in row.ljust(w)]) return g +def from_png(path, threshold=128): + """Load a PNG and convert to a 1-bpp grid (white/bright=lit, dark=off). + Width is padded to the nearest multiple of 8. Requires Pillow: pip install pillow""" + try: + from PIL import Image + except ImportError: + raise SystemExit("from_png() needs Pillow: pip install pillow") + img = Image.open(path).convert("L") + w, h = img.size + wp = (w + 7) // 8 * 8 + g = [] + for y in range(h): + row = [1 if x < w and img.getpixel((x, y)) > threshold else 0 for x in range(wp)] + g.append(row) + return g + +def load_species_pngs(prefix, aliases=None): + """Load the 7 expression PNGs for a species from _.png files. + Missing files fall back automatically (blink->idle, eat->happy, dead->sad). + Pass aliases={"blink": "idle"} to force a specific reuse. + Only custom_idle.png is strictly required; others are optional.""" + import os + EXPRS = ["idle", "blink", "happy", "sad", "eat", "sleep", "dead"] + FALLBACK = { "blink": "idle", "eat": "happy", "dead": "sad"} + if aliases: + FALLBACK.update(aliases) + cache = {} + result = [] + for name in EXPRS: + src = FALLBACK.get(name, name) + if src not in cache: + path = f"{prefix}_{src}.png" + if not os.path.exists(path): + path = f"{prefix}_idle.png" + cache[src] = from_png(path) + cache[name] = cache[src] + result.append(cache[name]) + return result + def pack(g): """Pack to bytes, MSB first, width padded to multiple of 8.""" h = len(g) @@ -395,6 +434,23 @@ def icon_settings(): # gear fill_ellipse(g, 8, 8, 2, 2, 0) # hub hole return g +def icon_ir(): # wifi-style arcs = IR signal + g = grid(16, 16) + vline(g, 8, 9, 14) # antenna stem + px(g, 7, 14); px(g, 9, 14) # base feet + # inner arc + px(g, 6, 8); px(g, 10, 8) + px(g, 7, 7); px(g, 9, 7) + px(g, 8, 6) + # outer arc + px(g, 3, 9); px(g, 13, 9) + px(g, 4, 7); px(g, 12, 7) + px(g, 5, 5); px(g, 11, 5) + px(g, 6, 4); px(g, 10, 4) + px(g, 7, 3); px(g, 9, 3) + px(g, 8, 2) + return g + # ---------------------------------------------------------------- main def main(): print("// Auto-generated by sprites_gen.py -- do not edit by hand.") @@ -442,17 +498,46 @@ def main(): # species frame tables, indexed [species][EXP_*]. Shell/Tmux reuse their # four frames across the seven expression slots. - print("#define SPECIES_COUNT 3") - print("enum { SP_SHELL, SP_PANKO, SP_TMUX };") + # + # Custom species: drop PNG files named custom_.png (32x32, white=lit) + # next to this script and re-run it. Required: custom_idle.png. + # Optional (fall back automatically): custom_blink, custom_happy, custom_sad, + # custom_eat, custom_sleep, custom_dead. + import os + has_custom = os.path.exists("custom_idle.png") + if has_custom: + EXPRS = ["idle", "blink", "happy", "sad", "eat", "sleep", "dead"] + custom_frames = load_species_pngs("custom") + for name, g in zip(EXPRS, custom_frames): + emit(f"custom_{name}", g) + + species_count = 4 if has_custom else 3 + print(f"#define SPECIES_COUNT {species_count}") + enum_vals = "SP_SHELL, SP_PANKO, SP_TMUX" + (", SP_CUSTOM" if has_custom else "") + print(f"enum {{ {enum_vals} }};") print("static const char *const species_names[SPECIES_COUNT] = {") - print(' "Shell", "Panko", "Tmux",') + if has_custom: + print(' "Shell", "Panko", "Tmux", "Custom",') + else: + print(' "Shell", "Panko", "Tmux",') print("};") print("static const unsigned char *const species_frames[SPECIES_COUNT][EXP_COUNT] = {") print(" { shell_idle, shell_idle, shell_happy, shell_sad, shell_happy, shell_sleep, shell_sad },") print(" { pana_idle, pana_blink, pana_happy, pana_sad, pana_eat, pana_sleep, pana_dead },") print(" { tmux_idle, tmux_idle, tmux_happy, tmux_sad, tmux_happy, tmux_sleep, tmux_sad },") + if has_custom: + print(" { custom_idle, custom_blink, custom_happy, custom_sad, custom_eat, custom_sleep, custom_dead },") print("};") print() + if has_custom: + # Emit eye geometry as defines so tama.c can extend its arrays. + # Edit these pixel coords to match where your character's eyes sit. + print("// Eye centre coords for custom species accessories (shades at user, headset at root).") + print("// Pixel positions inside the 32x32 sprite -- adjust to match your art.") + print("#define CUSTOM_EYE_CY 17 // eye centre row") + print("#define CUSTOM_EYE_LX 11 // left eye centre column") + print("#define CUSTOM_EYE_RX 21 // right eye centre column") + print() emit("spr_grave", grave()) emit("spr_poop", poop()) @@ -460,23 +545,24 @@ def main(): emit("spr_glitch", glitch()) icons = [ - ("icon_feed", icon_feed()), - ("icon_play", icon_play()), - ("icon_sleep", icon_sleep()), - ("icon_clean", icon_clean()), - ("icon_heal", icon_heal()), - ("icon_stats", icon_stats()), + ("icon_feed", icon_feed()), + ("icon_play", icon_play()), + ("icon_sleep", icon_sleep()), + ("icon_clean", icon_clean()), + ("icon_heal", icon_heal()), + ("icon_stats", icon_stats()), ("icon_settings", icon_settings()), + ("icon_ir", icon_ir()), ] for n, g in icons: emit(n, g) - print("#define ICON_COUNT 7") + print("#define ICON_COUNT 8") print("static const unsigned char *const menu_icons[ICON_COUNT] = {") - print(" icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings,") + print(" icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings, icon_ir,") print("};") print('static const char *const menu_labels[ICON_COUNT] = {') - print(' "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS",') + print(' "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS", "IR BEAM",') print("};") print() print("#endif // _SPRITES_H") diff --git a/tama.c b/tama.c index cf87a99..b1fb94d 100644 --- a/tama.c +++ b/tama.c @@ -32,6 +32,9 @@ #define BTN_RIGHT PD5 #define BTN_LEFT PD6 #define BUZZER PD2 +#define IR_TX PD4 // IR LED, bit-bang 38 kHz carrier +#define IR_RX PD3 // IR demodulator data (active-low) +#define IR_RX_VCC PC4 // IR receiver power gate (high = on) // ----------------------------------------------------------------- tuning #define FRAME_MS 60 // main loop period @@ -43,7 +46,7 @@ // a need empties in ~20 min, after which health bleeds down over ~10 more. // ----------------------------------------------------------------- state -enum { SCR_SELECT, SCR_MAIN, SCR_STATS, SCR_DEAD, SCR_SETTINGS }; +enum { SCR_SELECT, SCR_MAIN, SCR_STATS, SCR_DEAD, SCR_SETTINGS, SCR_IR, SCR_TV }; // notification thresholds: gentle chirp at <50%, urgent at <20% enum { N_FOOD, N_FUN, N_ENGY, N_WASH, N_COUNT }; @@ -107,6 +110,47 @@ static const char *const screen_timeout_names[] = { "5s", "10s", "15s", "30s", " #define TIMEOUT_OPT_COUNT 6 #define SETTINGS_ROWS 4 // screen-off, brightness, sound, back +// IR screen state +// Modes: 0=ZAPP (send ping), 1=RECV (listen), 2=BEAM (send pet data), 3=TV remote, 4=BACK +enum { IR_ZAPP, IR_RECV, IR_BEAM, IR_TV, IR_BACK, IR_MODE_COUNT }; +static uint8_t ir_mode = 0; +// ir_status: 0=idle, 1=sent, 2=got signal, 3=nothing, 4=waiting +static uint8_t ir_status = 0; +static uint8_t ir_stimer = 0; // frames to show a status message + +// NEC protocol constants for badge-to-badge comms +#define PANA_ADDR 0x50 // 'P' for Panagotchi + +// ----------------------------------------------------------------- TV remote +// Commands fired at every brand in sequence; TV responds to whichever matches. +// Order must match tv_cmd_names[]. BACK is a navigation item, not an IR code. +#define TV_CMD_COUNT 6 +#define TV_NAV_COUNT 7 // TV_CMD_COUNT + 1 (BACK entry) +#define TV_BRAND_COUNT 5 + +static const char *const tv_cmd_names[TV_NAV_COUNT] = { + "POWER", "VOL+", "VOL-", "MUTE", "CH+", "CH-", "BACK", +}; + +// NEC addresses per brand +static const uint8_t tv_addr[TV_BRAND_COUNT] = { + 0x04, // LG + 0x40, // Panasonic + 0xA5, // Pioneer + 0x05, // Mitsubishi + 0x00, // Generic / CHUNGHWA +}; +// Commands: [brand][POWER, VOL+, VOL-, MUTE, CH+, CH-] +static const uint8_t tv_cmds[TV_BRAND_COUNT][TV_CMD_COUNT] = { + { 0x08, 0x02, 0x03, 0x09, 0x00, 0x01 }, // LG + { 0x3D, 0x20, 0x21, 0x04, 0x34, 0x35 }, // Panasonic + { 0x1C, 0x14, 0x15, 0x19, 0x01, 0x02 }, // Pioneer + { 0x18, 0x07, 0x0B, 0x0D, 0x02, 0x06 }, // Mitsubishi + { 0x45, 0x46, 0x15, 0x09, 0x09, 0x15 }, // Generic +}; + +static uint8_t tv_cmd = 0; // currently selected command (0..TV_NAV_COUNT-1) + // display power state static uint8_t disp_on = 1; static uint32_t idle_frames = 0; // frames since the last button activity @@ -115,7 +159,7 @@ static uint32_t idle_frames = 0; // frames since the last button activity static uint32_t reset_cause = 0; // menu icon order must match sprites.h menu_icons[] / menu_labels[] -enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS, ACT_SETTINGS }; +enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS, ACT_SETTINGS, ACT_IR }; // ----------------------------------------------------------------- helpers static int8_t clamp100(int v) { return v < 0 ? 0 : (v > 100 ? 100 : v); } @@ -156,6 +200,90 @@ static void led_blink(void) funDigitalWrite(LED_PIN, FUN_LOW); } +// ----------------------------------------------------------------- IR TX +// Bit-bang 38 kHz carrier on IR_TX for `us` microseconds (mark). +static void ir_mark(uint16_t us) +{ + // half-period ≈ 13 µs → cycles ≈ us/26 + uint16_t cycles = us / 26u + 1u; + for (uint16_t i = 0; i < cycles; i++) { + funDigitalWrite(IR_TX, FUN_HIGH); + Delay_Us(13); + funDigitalWrite(IR_TX, FUN_LOW); + Delay_Us(13); + } +} + +static void ir_space(uint16_t us) +{ + funDigitalWrite(IR_TX, FUN_LOW); + Delay_Us(us); +} + +// Send one NEC frame: 9ms mark + 4.5ms space + 32 bits + stop. +static void ir_send_nec(uint8_t addr, uint8_t cmd) +{ + ir_mark(9000); ir_space(4500); + uint8_t bytes[4] = { addr, (uint8_t)~addr, cmd, (uint8_t)~cmd }; + for (uint8_t b = 0; b < 4; b++) { + for (uint8_t i = 0; i < 8; i++) { + ir_mark(562); + ir_space((bytes[b] & 1) ? 1687u : 562u); + bytes[b] >>= 1; + } + } + ir_mark(562); + funDigitalWrite(IR_TX, FUN_LOW); +} + +// ----------------------------------------------------------------- IR RX +// Wait up to `wait_us` for IR_RX to reach `lvl`, then measure how long it +// holds (up to `hold_us`). Returns ~duration in µs (10 µs resolution), or 0 +// on timeout. The IR demodulator output is active-low: LOW = carrier present. +static uint32_t ir_pulse(uint8_t lvl, uint32_t wait_us, uint32_t hold_us) +{ + uint32_t t; + for (t = 0; t < wait_us; t += 10) { + if ((uint8_t)funDigitalRead(IR_RX) == lvl) goto measure; + Delay_Us(10); + } + return 0; +measure:; + for (t = 0; t < hold_us; t += 10) { + if ((uint8_t)funDigitalRead(IR_RX) != lvl) return t; + Delay_Us(10); + } + return 0; // pulse held too long +} + +// Try to receive one NEC frame. Blocks up to ~2 s waiting for the leader. +// Returns 1 on a valid frame, 0 otherwise. addr/cmd are set on success. +static uint8_t ir_recv_nec(uint8_t *addr_out, uint8_t *cmd_out) +{ + // leader mark ~9000 µs LOW; wait up to 2 s + uint32_t t = ir_pulse(FUN_LOW, 2000000u, 12000u); + if (t < 7000u) return 0; + + // leader space ~4500 µs HIGH + t = ir_pulse(FUN_HIGH, 8000u, 6000u); + if (t < 3000u) return 0; + + uint32_t bits = 0; + for (uint8_t i = 0; i < 32; i++) { + if (!ir_pulse(FUN_LOW, 1500u, 1500u)) return 0; + t = ir_pulse(FUN_HIGH, 2500u, 2500u); + if (!t) return 0; + bits >>= 1; + if (t > 1000u) bits |= 0x80000000UL; + } + ir_pulse(FUN_LOW, 1500u, 1500u); // stop bit + + *addr_out = (uint8_t)(bits & 0xFF); + *cmd_out = (uint8_t)((bits >> 16) & 0xFF); + return ((bits >> 8) & 0xFF) == (uint8_t)(~*addr_out) && + ((bits >> 24) & 0xFF) == (uint8_t)(~*cmd_out); +} + // push the currently selected brightness to the panel's contrast register static void apply_brightness(void) { @@ -315,6 +443,14 @@ static void do_action(uint8_t act) set_sel = 0; beep(659, 40); break; + + case ACT_IR: + screen = SCR_IR; + ir_mode = 0; + ir_status = 0; + ir_stimer = 0; + beep(659, 40); + break; } } @@ -378,19 +514,27 @@ static void draw_center_str(int y, const char *s) static void draw_menu_bar(void) { - // 7 icons across the bottom (18 px slots), selected one boxed + // 8 icons across the bottom (16 px slots, no gap), selected one boxed for (uint8_t i = 0; i < ICON_COUNT; i++) { - int x = i * 18 + 1; + int x = i * 16; ssd1306_drawImage(x, 48, menu_icons[i], ICON_W, ICON_H, 0); if (i == cursor) - ssd1306_drawRect(x - 1, 47, ICON_W + 1, ICON_H + 1, 1); + ssd1306_drawRect(x, 47, ICON_W, ICON_H + 1, 1); } } -// Per-species eye geometry, so the shades land on the right spot. -static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; // SP_SHELL,PANKO,TMUX +// Per-species eye geometry for accessory rendering (shades at user, headset at root). +// When a custom species is present, sprites.h emits CUSTOM_EYE_* defines; update +// those values to match where the eyes sit in your custom_idle.png. +#ifdef CUSTOM_EYE_CY +static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12, CUSTOM_EYE_CY }; +static const uint8_t eye_lx[SPECIES_COUNT] = { 11, 11, 9, CUSTOM_EYE_LX }; +static const uint8_t eye_rx[SPECIES_COUNT] = { 20, 21, 23, CUSTOM_EYE_RX }; +#else +static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; static const uint8_t eye_lx[SPECIES_COUNT] = { 11, 11, 9 }; static const uint8_t eye_rx[SPECIES_COUNT] = { 20, 21, 23 }; +#endif // Hacker garb earned as the pet levels up, drawn dark over the lit body so it // reads as black gear. @@ -579,6 +723,144 @@ static void draw_settings(void) ssd1306_refresh(); } +static const char *const ir_mode_names[IR_MODE_COUNT] = { + "ZAPP", "RECV", "BEAM", "TV", "BACK", +}; + +static void draw_ir(void) +{ + ssd1306_setbuf(0); + draw_center_str(0, "~ IR BEAM ~"); + ssd1306_drawFastHLine(0, 10, SSD1306_W, 1); + + // mode selector with arrows + char mline[17], *p = mline; + p = s_cat(p, "< "); + p = s_cat(p, ir_mode_names[ir_mode]); + p = s_cat(p, " >"); + *p = 0; + draw_center_str(20, mline); + + // status line + if (ir_stimer) { + const char *msg = "?"; + if (ir_status == 1) msg = "SENT!"; + else if (ir_status == 2) msg = "GOT IT! +xp"; + else if (ir_status == 3) msg = "NOTHING"; + else if (ir_status == 4) msg = "WAITING..."; + draw_center_str(35, msg); + } else { + const char *hint = "?"; + if (ir_mode == IR_ZAPP) hint = "OK: send ping"; + else if (ir_mode == IR_RECV) hint = "OK: listen 2s"; + else if (ir_mode == IR_BEAM) hint = "OK: beam pet"; + else if (ir_mode == IR_TV) hint = "OK: enter"; + else if (ir_mode == IR_BACK) hint = "OK: back"; + draw_center_str(35, hint); + } + + draw_center_str(56, "L/R mode OK go"); + ssd1306_refresh(); +} + +// Called when OK is pressed on the IR screen. +static void do_ir_action(void) +{ + if (ir_mode == IR_BACK) { + screen = SCR_MAIN; + beep(880, 30); + return; + } + + if (ir_mode == IR_ZAPP) { + ir_send_nec(PANA_ADDR, 0x01); // fixed ping command + beep(988, 40); beep(1319, 60); + led_blink(); + ir_status = 1; ir_stimer = 30; + return; + } + + if (ir_mode == IR_BEAM) { + // encode species in high nibble, level in low nibble + uint8_t cmd = (uint8_t)((pet.species << 4) | (pet.level & 0x0F)); + ir_send_nec(PANA_ADDR, cmd); + beep(880, 40); beep(1175, 40); beep(1568, 70); + led_blink(); + ir_status = 1; ir_stimer = 30; + return; + } + + if (ir_mode == IR_TV) { + screen = SCR_TV; + tv_cmd = 0; + beep(659, 40); + return; + } + + if (ir_mode == IR_RECV) { + // show WAITING... while blocking + ir_status = 4; ir_stimer = 255; + draw_ir(); + + // power up the IR receiver + funDigitalWrite(IR_RX_VCC, FUN_HIGH); + Delay_Ms(5); // stabilise + + uint8_t addr = 0, cmd = 0; + uint8_t ok = ir_recv_nec(&addr, &cmd); + + funDigitalWrite(IR_RX_VCC, FUN_LOW); // power down receiver + + if (ok) { + if (addr == PANA_ADDR) { + if (cmd == 0x01) { + // ping from another badge: mood boost + pet.happy = clamp100(pet.happy + 10); + add_xp(5); + } else { + // BEAM packet: species in high nibble, level in low nibble + uint8_t lvl = cmd & 0x0F; + if (lvl > pet.level) add_xp(8); else add_xp(3); + pet.happy = clamp100(pet.happy + 8); + } + } else { + // generic NEC signal: small happy bump + pet.happy = clamp100(pet.happy + 3); + } + beep(784, 60); beep(988, 60); beep(1175, 90); + led_blink(); + ir_status = 2; + } else { + beep(330, 150); + ir_status = 3; + } + ir_stimer = 45; + } +} + +static void draw_tv(void) +{ + ssd1306_setbuf(0); + draw_center_str(0, "~ TV REMOTE ~"); + ssd1306_drawFastHLine(0, 10, SSD1306_W, 1); + + // command selector + char line[17], *p = line; + p = s_cat(p, "< "); + p = s_cat(p, tv_cmd_names[tv_cmd]); + p = s_cat(p, " >"); + *p = 0; + draw_center_str(22, line); + + if (tv_cmd < TV_CMD_COUNT) + draw_center_str(36, "blast 5 brands"); + // BACK entry has no sub-label + + ssd1306_drawFastHLine(0, 46, SSD1306_W, 1); + draw_center_str(50, "L/R cmd OK go"); + ssd1306_refresh(); +} + // ----------------------------------------------------------------- input typedef struct { uint8_t ok, left, right; } Buttons; @@ -685,6 +967,15 @@ static void gpio_setup(void) funPinMode(BUZZER, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); funDigitalWrite(BUZZER, FUN_LOW); + funPinMode(IR_TX, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(IR_TX, FUN_LOW); + + funPinMode(IR_RX_VCC, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(IR_RX_VCC, FUN_LOW); // receiver off at boot + + funPinMode(IR_RX, GPIO_CNF_IN_PUPD); + funDigitalWrite(IR_RX, FUN_HIGH); // pull-up (idle = HIGH) + // buttons: input with internal pull-up (ODR high selects pull-up) funPinMode(BTN_OK, GPIO_CNF_IN_PUPD); funPinMode(BTN_LEFT, GPIO_CNF_IN_PUPD); @@ -851,6 +1142,46 @@ int main(void) } draw_dead(); break; + + case SCR_IR: + if (left_edge) { + ir_mode = (ir_mode + IR_MODE_COUNT - 1) % IR_MODE_COUNT; + beep(1047, 25); + } + if (right_edge) { + ir_mode = (ir_mode + 1) % IR_MODE_COUNT; + beep(1047, 25); + } + if (ok_edge) do_ir_action(); + if (ir_stimer) ir_stimer--; + draw_ir(); + break; + + case SCR_TV: + if (left_edge) { + tv_cmd = (tv_cmd + TV_NAV_COUNT - 1) % TV_NAV_COUNT; + beep(1047, 25); + } + if (right_edge) { + tv_cmd = (tv_cmd + 1) % TV_NAV_COUNT; + beep(1047, 25); + } + if (ok_edge) { + if (tv_cmd == TV_CMD_COUNT) { // BACK entry + screen = SCR_IR; + beep(880, 30); + } else { + // blast selected command at all brands in turn + for (uint8_t b = 0; b < TV_BRAND_COUNT; b++) { + ir_send_nec(tv_addr[b], tv_cmds[b][tv_cmd]); + Delay_Ms(40); // inter-frame gap + } + beep(988, 40); beep(1319, 80); + led_blink(); + } + } + draw_tv(); + break; } // sleep the display after the configured idle period