From 8bc92ed51345a85d2aba321efcbd6a1be0ac0a61 Mon Sep 17 00:00:00 2001 From: charlotteones <122920123+chrlarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:24:28 +0200 Subject: [PATCH] 1 --- Blink copy.png | Bin 0 -> 271 bytes README.md | 92 ++++++++ custom_blink.png | Bin 0 -> 271 bytes custom_dead.png | Bin 0 -> 265 bytes custom_eat.png | Bin 0 -> 278 bytes custom_happy.png | Bin 0 -> 275 bytes custom_idle.png | Bin 0 -> 282 bytes custom_sad.png | Bin 0 -> 280 bytes custom_sleep.png | Bin 0 -> 258 bytes sprites.h | 599 ++++++++++++++++++++++++++++++++++++++++------- sprites_gen.py | 110 ++++++++- tama.c | 345 ++++++++++++++++++++++++++- 12 files changed, 1040 insertions(+), 106 deletions(-) create mode 100644 Blink copy.png create mode 100644 README.md create mode 100644 custom_blink.png create mode 100644 custom_dead.png create mode 100644 custom_eat.png create mode 100644 custom_happy.png create mode 100644 custom_idle.png create mode 100644 custom_sad.png create mode 100644 custom_sleep.png diff --git a/Blink copy.png b/Blink copy.png new file mode 100644 index 0000000000000000000000000000000000000000..b542dbf66b705f193ee28e5eebe53b64d19e3531 GIT binary patch literal 271 zcmV+q0r38bP)kdg00001b5ch_0Itp) z=>Px#$w@>(R7i=%R8bB?AqbP@-T%tn$Cf<{Ezy}}{1~A?L1c`7OBg*iu#4_ay9Hbd z(SwThahA=y;k!BA5zk^x1_uF6lCRVSe5!#`OLahKQ>3iD)`tY(my19yWfPx _` 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 0000000000000000000000000000000000000000..b542dbf66b705f193ee28e5eebe53b64d19e3531 GIT binary patch literal 271 zcmV+q0r38bP)kdg00001b5ch_0Itp) z=>Px#$w@>(R7i=%R8bB?AqbP@-T%tn$Cf<{Ezy}}{1~A?L1c`7OBg*iu#4_ay9Hbd z(SwThahA=y;k!BA5zk^x1_uF6lCRVSe5!#`OLahKQ>3iD)`tY(my19yWfPxk4UjKx9jP7LeL$-D$|4tu&dhIn+o z4Y??ESb@X%_y78HaW>qFJ9gQc+p4?j;@l(NY(C*%GWmyib$SK5}i_nA3!?);}0 zuT${BX_5k~73+yR8HavLSv;Dmzkdg00001b5ch_0Itp) z=>Px#&`Cr=R7i=%RM8T{AP6hH{r@j#FRKz_oVB+bcfD3fFh$`w{%v9WxPe`yJMS&v zQi?f9qUW>SJPmu50KFY_lA;n9G`t4kXpo@QwI+*3Q(sRwRumn~psc{qI_qrGc#jZD zd>&*vOXU)RidoO6#d;BAlyzu?iV{HbLQv|FV`OSx!JJtU*S|p3$n_z*HWTPK@*nf1 znrhINsw$dk4H)qCTr`~fU;*zFEN-bqyfJ%caHnHqw9luL9jdwKw#KX8Jd?(c^@oPi c8NXBT1|O4R?7(PgZ~y=R07*qoM6N<$f+@3h`v3p{ literal 0 HcmV?d00001 diff --git a/custom_happy.png b/custom_happy.png new file mode 100644 index 0000000000000000000000000000000000000000..c8544cdb93fb6be0ac4fc1590e3b79a32797af94 GIT binary patch literal 275 zcmV+u0qp*XP)kdg00001b5ch_0Itp) z=>Px#%}GQ-R7i=%R5=dAFbuPb{{PGDP{7tfs!B3cOAwtDX?v}In+ZQISA8SXKG@Kve7|E3AHTbL^u$cO?SZPaxI@f|!A{MTEN8I-h;WhuY}1EHD@VvyBJqLY@)uh=XKLfCTDH3unTh-}2o)BJp$ zt(<~-nu5dZHLjvkOT)Ee#Q4ZNhHg?iR!_i{A9{*4ff Z?E|8WYT9FFO@9CY002ovPDHLkV1l-jc5MIv literal 0 HcmV?d00001 diff --git a/custom_idle.png b/custom_idle.png new file mode 100644 index 0000000000000000000000000000000000000000..90a0d4824c5e46361d3905a9fe7283f02f480beb GIT binary patch literal 282 zcmV+#0pkdg00001b5ch_0Itp) z=>Px#)Ja4^R7i=%R6!2IFbIpQ{r}7CvIYz$N>)uns;Ynu1Oj9HThQq30J}-=v`4@# z7i&agB2}P6;GH=ya3bhW4C4Tii?YyDA)+46El*1c?_=x z-#o?7AberdY*}@3sM)#NIk%8?UB^Dje~xz gKNKsE=~o%N0l#)(?b{y;1^@s607*qoM6N<$f=Bmr`Tzg` literal 0 HcmV?d00001 diff --git a/custom_sad.png b/custom_sad.png new file mode 100644 index 0000000000000000000000000000000000000000..c01e8371c0053fd2ee615ebca5af9655a3fbb887 GIT binary patch literal 280 zcmV+z0q6dSP)kdg00001b5ch_0Itp) z=>Px#(n&-?R7i={RY4L0F$g1$|Nk;xv<3*Y>z?GYQwSlYEo=7ohQ{v~u!rfL_Y&}k zVg{Ay<6gBs&425Jhi?ZRD01Ru4OMss7X$K&RdmwRT%hKOuxM6N)G+l&#z*S?Wbm@zsA1Cs3pHZT zvx;T;IfJT29nivW&qPhoy*GhY1k=bUcsztIJ6huGilVm&Oh`30000kdg00001b5ch_0Itp) z=>Px#yh%hsR7i=%R7)1bAP57W_rEf;sTFGYXlLHAX|*JT6eaq1a5+;BFbUCj;J%zy zvYg46$Fru_?xioNcKj3+;!PVKe0p+Ck0>INj1NM>^srbxJuwS z0}3Y7!hoaHKzF+rv-(OAnuv!-?}pCso$~D8LfG5yj%Z6L;O&a8{(**~vk2R0W!K#+ zEkE~Bfy;xLm 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