Compare commits

..

No commits in common. "pan" and "master" have entirely different histories.
pan ... master

12 changed files with 106 additions and 1040 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

View File

@ -1,92 +0,0 @@
# 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 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

599
sprites.h
View File

@ -9,254 +9,108 @@
// pana_idle: 32x32 // pana_idle: 32x32
static const unsigned char pana_idle[] = { static const unsigned char pana_idle[] = {
0x00, 0x00, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xF7, 0xEF, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xFC, 0x3F, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x7B, 0xDE, 0x00, 0x01, 0xF7, 0xEF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xFC, 0xBF, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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, 0x00, 0x00, 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,
0x01, 0x00, 0x00, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xF8, 0x3F, 0x00,
0x0f, 0xe0, 0x07, 0xf0, 0x00, 0xE7, 0xC7, 0x00, 0x00, 0xDF, 0xFB, 0x00, 0x01, 0xBF, 0xFD, 0x80,
0x1f, 0xf0, 0x0f, 0xf8, 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,
0x3f, 0xf8, 0x9f, 0xfc, 0x00, 0xF7, 0x7F, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x1f, 0xff, 0xff, 0xf8, 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,
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 };
@ -552,276 +406,17 @@ static const unsigned char tmux_sleep[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}; };
// custom_idle: 32x32 #define SPECIES_COUNT 3
static const unsigned char custom_idle[] = { enum { SP_SHELL, SP_PANKO, SP_TMUX };
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
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", "Custom", "Shell", "Panko", "Tmux",
}; };
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,
@ -1082,32 +677,12 @@ static const unsigned char icon_settings[] = {
0x03, 0xc0, 0x03, 0xc0,
}; };
// icon_ir: 16x16 #define ICON_COUNT 7
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_ir, icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings,
}; };
static const char *const menu_labels[ICON_COUNT] = { static const char *const menu_labels[ICON_COUNT] = {
"FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS", "IR BEAM", "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS",
}; };
#endif // _SPRITES_H #endif // _SPRITES_H

View File

@ -52,45 +52,6 @@ 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)
@ -434,23 +395,6 @@ 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.")
@ -498,46 +442,17 @@ 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")
# Custom species: drop PNG files named custom_<expr>.png (32x32, white=lit) print("enum { SP_SHELL, SP_PANKO, SP_TMUX };")
# 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",')
print(' "Shell", "Panko", "Tmux", "Custom",')
else:
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())
@ -545,24 +460,23 @@ def main():
emit("spr_glitch", glitch()) emit("spr_glitch", glitch())
icons = [ icons = [
("icon_feed", icon_feed()), ("icon_feed", icon_feed()),
("icon_play", icon_play()), ("icon_play", icon_play()),
("icon_sleep", icon_sleep()), ("icon_sleep", icon_sleep()),
("icon_clean", icon_clean()), ("icon_clean", icon_clean()),
("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 8") print("#define ICON_COUNT 7")
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, icon_ir,") print(" icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats, icon_settings,")
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", "IR BEAM",') print(' "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS", "SETTINGS",')
print("};") print("};")
print() print()
print("#endif // _SPRITES_H") print("#endif // _SPRITES_H")

345
tama.c
View File

@ -32,9 +32,6 @@
#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
@ -46,7 +43,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, SCR_IR, SCR_TV }; enum { SCR_SELECT, SCR_MAIN, SCR_STATS, SCR_DEAD, SCR_SETTINGS };
// 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 };
@ -110,47 +107,6 @@ 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
@ -159,7 +115,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, ACT_IR }; enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS, ACT_SETTINGS };
// ----------------------------------------------------------------- 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); }
@ -200,90 +156,6 @@ 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)
{ {
@ -443,14 +315,6 @@ 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;
} }
} }
@ -514,27 +378,19 @@ static void draw_center_str(int y, const char *s)
static void draw_menu_bar(void) static void draw_menu_bar(void)
{ {
// 8 icons across the bottom (16 px slots, no gap), selected one boxed // 7 icons across the bottom (18 px slots), selected one boxed
for (uint8_t i = 0; i < ICON_COUNT; i++) { for (uint8_t i = 0; i < ICON_COUNT; i++) {
int x = i * 16; int x = i * 18 + 1;
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, 47, ICON_W, ICON_H + 1, 1); ssd1306_drawRect(x - 1, 47, ICON_W + 1, ICON_H + 1, 1);
} }
} }
// Per-species eye geometry for accessory rendering (shades at user, headset at root). // Per-species eye geometry, so the shades land on the right spot.
// When a custom species is present, sprites.h emits CUSTOM_EYE_* defines; update static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; // SP_SHELL,PANKO,TMUX
// 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.
@ -723,144 +579,6 @@ 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;
@ -967,15 +685,6 @@ 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);
@ -1142,46 +851,6 @@ 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