Compare commits

...

1 Commits
master ... pan

Author SHA1 Message Date
charlotteones 8bc92ed513 1 2026-07-21 00:24:28 +02:00
12 changed files with 1040 additions and 106 deletions

BIN
Blink copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

92
README.md Normal file
View File

@ -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 |

BIN
custom_blink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

BIN
custom_dead.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

BIN
custom_eat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

BIN
custom_happy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

BIN
custom_idle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

BIN
custom_sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

BIN
custom_sleep.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

595
sprites.h
View File

@ -9,108 +9,254 @@
// pana_idle: 32x32 // pana_idle: 32x32
static const unsigned char pana_idle[] = { static const unsigned char pana_idle[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFC, 0x3F, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_blink: 32x32
static const unsigned char pana_blink[] = { static const unsigned char pana_blink[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xC3, 0xC3, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFC, 0x3F, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_happy: 32x32
static const unsigned char pana_happy[] = { static const unsigned char pana_happy[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xEF, 0xF7, 0xC0, 0x03, 0xE7, 0xEB, 0xC0, 0x03, 0xDB, 0xDB, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xF7, 0xEF, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xF7, 0xEF, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xFC, 0x3F, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_sad: 32x32
static const unsigned char pana_sad[] = { static const unsigned char pana_sad[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0x9F, 0xF9, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xFC, 0x3F, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xF7, 0xEF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_eat: 32x32
static const unsigned char pana_eat[] ={ static const unsigned char pana_eat[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFC, 0x3F, 0x80, 0x01, 0xFD, 0x3F, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xFC, 0xBF, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_sleep: 32x32
static const unsigned char pana_sleep[] = { static const unsigned char pana_sleep[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x20, 0x00, 0x39, 0xDC, 0x40, 0x00, 0x3B, 0xDC, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xC3, 0xC3, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x01, 0xFE, 0x7F, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 // pana_dead: 32x32
static const unsigned char pana_dead[] = { static const unsigned char pana_dead[] = {
0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x88, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x31, 0x8C, 0x00, 0x00, 0x39, 0xDC, 0x00, 0x00, 0x3B, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x01, 0x00, 0x00, 0x80,
0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80, 0x0f, 0xe0, 0x07, 0xf0,
0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xDB, 0xDB, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x03, 0xE7, 0xE7, 0xC0, 0x1f, 0xf0, 0x0f, 0xf8,
0x03, 0xDB, 0xDB, 0xC0, 0x01, 0xFD, 0xDF, 0x80, 0x01, 0xFA, 0xBF, 0x80, 0x1f, 0xf0, 0x0f, 0xf8,
0x00, 0xF7, 0x7F, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x3f, 0xf8, 0x9f, 0xfc,
0x01, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0x1f, 0xff, 0xff, 0xf8,
0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF0, 0x1f, 0xff, 0xff, 0xf8,
0x0F, 0xFF, 0xFF, 0xF8, 0x0F, 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 }; 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, 0x00, 0x00, 0x00, 0x00,
}; };
#define SPECIES_COUNT 3 // custom_idle: 32x32
enum { SP_SHELL, SP_PANKO, SP_TMUX }; 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] = { 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] = { 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 }, { 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 }, { 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 }, { 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 // spr_grave: 32x32
static const unsigned char spr_grave[] = { static const unsigned char spr_grave[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -677,12 +1082,32 @@ static const unsigned char icon_settings[] = {
0x03, 0xc0, 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] = { 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] = { 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 #endif // _SPRITES_H

View File

@ -52,6 +52,45 @@ def from_art(art):
g.append([1 if c in "#X1" else 0 for c in row.ljust(w)]) g.append([1 if c in "#X1" else 0 for c in row.ljust(w)])
return g 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 <prefix>_<expr>.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): def pack(g):
"""Pack to bytes, MSB first, width padded to multiple of 8.""" """Pack to bytes, MSB first, width padded to multiple of 8."""
h = len(g) h = len(g)
@ -395,6 +434,23 @@ def icon_settings(): # gear
fill_ellipse(g, 8, 8, 2, 2, 0) # hub hole fill_ellipse(g, 8, 8, 2, 2, 0) # hub hole
return g 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 # ---------------------------------------------------------------- main
def main(): def main():
print("// Auto-generated by sprites_gen.py -- do not edit by hand.") 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 # species frame tables, indexed [species][EXP_*]. Shell/Tmux reuse their
# four frames across the seven expression slots. # 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_<expr>.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("static const char *const species_names[SPECIES_COUNT] = {")
if has_custom:
print(' "Shell", "Panko", "Tmux", "Custom",')
else:
print(' "Shell", "Panko", "Tmux",') print(' "Shell", "Panko", "Tmux",')
print("};") print("};")
print("static const unsigned char *const species_frames[SPECIES_COUNT][EXP_COUNT] = {") 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(" { 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(" { 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 },") 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("};")
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_grave", grave())
emit("spr_poop", poop()) emit("spr_poop", poop())
@ -467,16 +552,17 @@ def main():
("icon_heal", icon_heal()), ("icon_heal", icon_heal()),
("icon_stats", icon_stats()), ("icon_stats", icon_stats()),
("icon_settings", icon_settings()), ("icon_settings", icon_settings()),
("icon_ir", icon_ir()),
] ]
for n, g in icons: for n, g in icons:
emit(n, g) emit(n, g)
print("#define ICON_COUNT 7") print("#define ICON_COUNT 8")
print("static const unsigned char *const menu_icons[ICON_COUNT] = {") 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("};")
print('static const char *const menu_labels[ICON_COUNT] = {') 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() print()
print("#endif // _SPRITES_H") print("#endif // _SPRITES_H")

345
tama.c
View File

@ -32,6 +32,9 @@
#define BTN_RIGHT PD5 #define BTN_RIGHT PD5
#define BTN_LEFT PD6 #define BTN_LEFT PD6
#define BUZZER PD2 #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 // ----------------------------------------------------------------- tuning
#define FRAME_MS 60 // main loop period #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. // a need empties in ~20 min, after which health bleeds down over ~10 more.
// ----------------------------------------------------------------- state // ----------------------------------------------------------------- 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% // notification thresholds: gentle chirp at <50%, urgent at <20%
enum { N_FOOD, N_FUN, N_ENGY, N_WASH, N_COUNT }; 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 TIMEOUT_OPT_COUNT 6
#define SETTINGS_ROWS 4 // screen-off, brightness, sound, back #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 // display power state
static uint8_t disp_on = 1; static uint8_t disp_on = 1;
static uint32_t idle_frames = 0; // frames since the last button activity 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; static uint32_t reset_cause = 0;
// menu icon order must match sprites.h menu_icons[] / menu_labels[] // 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 // ----------------------------------------------------------------- helpers
static int8_t clamp100(int v) { return v < 0 ? 0 : (v > 100 ? 100 : v); } 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); 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 // push the currently selected brightness to the panel's contrast register
static void apply_brightness(void) static void apply_brightness(void)
{ {
@ -315,6 +443,14 @@ static void do_action(uint8_t act)
set_sel = 0; set_sel = 0;
beep(659, 40); beep(659, 40);
break; 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) 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++) { 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); ssd1306_drawImage(x, 48, menu_icons[i], ICON_W, ICON_H, 0);
if (i == cursor) 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. // Per-species eye geometry for accessory rendering (shades at user, headset at root).
static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; // SP_SHELL,PANKO,TMUX // 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_lx[SPECIES_COUNT] = { 11, 11, 9 };
static const uint8_t eye_rx[SPECIES_COUNT] = { 20, 21, 23 }; 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 // Hacker garb earned as the pet levels up, drawn dark over the lit body so it
// reads as black gear. // reads as black gear.
@ -579,6 +723,144 @@ static void draw_settings(void)
ssd1306_refresh(); 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 // ----------------------------------------------------------------- input
typedef struct { uint8_t ok, left, right; } Buttons; 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); funPinMode(BUZZER, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
funDigitalWrite(BUZZER, FUN_LOW); 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) // buttons: input with internal pull-up (ODR high selects pull-up)
funPinMode(BTN_OK, GPIO_CNF_IN_PUPD); funPinMode(BTN_OK, GPIO_CNF_IN_PUPD);
funPinMode(BTN_LEFT, GPIO_CNF_IN_PUPD); funPinMode(BTN_LEFT, GPIO_CNF_IN_PUPD);
@ -851,6 +1142,46 @@ int main(void)
} }
draw_dead(); draw_dead();
break; 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 // sleep the display after the configured idle period