← Back to PCB page

CAP-MIDI

Finished PCB

Fabrication Files

Traces

ldo_trace.png

Holes

ldo_holes.png

Outline/Cutout

ldo_cut.png

Assembly

Describe your soldering process here. Mention the components used (e.g., LDO, capacitors, headers) and any challenges faced during the reflow or hand-soldering process.

Programming Process

Explain how you flashed the firmware. For the Xiao ESP32-C3 or RP2040, describe the bootloader mode and the environment used (Arduino IDE, PlatformIO, or CircuitPython).

KiCAT Tamagotchi code (micropython)


### Controls for your KiCAT:
#- **Feed - Pad 1:** Boosts hunger level.
#- **Play - Pad 6:** Boosts boredom/happiness level by playing jump game.
#- **Peek on Status- Pad 4:** Displays the current percentage levels in a small box at the top.

import utime
import urandom
from machine import Pin, I2C, freq
from ssd1306 import SSD1306_I2C
from ws2812 import WS2812
from steptime import STEPTIME
import framebuf

# ----- Hardware Setup -----
freq(250000000)
i2c = I2C(1, scl=Pin(7), sda=Pin(6), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)
power = Pin(11, Pin.OUT)
power.value(1)
led = WS2812(12, 1, 0.5, 6)

# Pad Mapping
PAD_PINS = [2, 4, 3, 1, 27, 26] 
for p in PAD_PINS: 
    Pin(p, Pin.IN, Pin.PULL_UP)
channels = [[STEPTIME(i, pin), [1e6], [0]] for i, pin in enumerate(PAD_PINS)]

# --- Game Bitmaps ---
CAT_A = bytearray([
    0,16,64,0,56,224,0,105,160,0,201,32,1,159,160,227,
255,224,243,255,224,54,255,232,119,127,240,110,111,120,111,239,
112,126,127,248,63,255,224,15,255,192,1,129,128,1,129,128
])

CAT_B = bytearray([
   0,16,64,0,56,224,0,105,160,0,201,32,1,159,160,227,
255,224,243,255,224,54,255,232,119,127,240,110,111,120,111,239,
112,126,127,248,63,255,224,15,255,192,3,0,192,6,0,96

])

fb_a = framebuf.FrameBuffer(CAT_A, 24, 16, framebuf.MONO_HLSB)
fb_b = framebuf.FrameBuffer(CAT_B, 24, 16, framebuf.MONO_HLSB)

# --- Stats & State ---
STATE_PET, STATE_GAME = 0, 1
mode = STATE_PET
hunger_lvl, boredom_lvl = 100, 100
last_tick = utime.ticks_ms()

# --- Physics Constants ---
GRAVITY = 4        #
JUMP_VEL = -18     #
GROUND_Y = 40
cat_y, cat_v, obs_x, game_score = GROUND_Y, 0, 128, 0

def show_rotated(display):
    rotated_buffer = bytearray(1024)
    rotated_fb = framebuf.FrameBuffer(rotated_buffer, 128, 64, framebuf.MONO_VLSB)
    for y in range(64):
        for x in range(128):
            if display.pixel(x, y):
                rotated_fb.pixel(127 - x, 63 - y, 1)
    display.i2c.writeto_mem(display.addr, 0x40, rotated_buffer)

def draw_full_face(expression="neutral", blink=False):
    oled.fill(0)
    size = 14
    if expression == "critical":
        for i in range(2):
            oled.line(20+i, 25, 20+size+i, 25+size, 1)
            oled.line(20+i, 25+size, 20+size+i, 25, 1)
            oled.line(94+i, 25, 94+size+i, 25+size, 1)
            oled.line(94+i, 25+size, 94+size+i, 25, 1)
    elif expression == "sad":
        # Extra thick Worried Eyes (/  \)
        for i in range(3):
            oled.line(20, 38+i, 20+size, 32+i, 1) 
            oled.line(94, 32+i, 94+size, 38+i, 1)
    elif blink:
        oled.fill_rect(20, 32, size, 2, 1)
        oled.fill_rect(94, 32, size, 2, 1)
    else:
        oled.fill_rect(20, 25, size, size, 1)
        oled.fill_rect(94, 25, size, size, 1)
    
    m_y = 33 if expression == "happy" else 44 if expression in ["sad", "critical"] else 38
    oled.fill_rect(57, m_y-13, size, size, 1) 
    oled.fill_rect(44, m_y, size, size, 1)    
    oled.fill_rect(70, m_y, size, size, 1)

while True:
    now = utime.ticks_ms()
    deltas = []
    for ch in channels:
        sm, min_val, _ = ch
        sm.put(200); sm.put(20000)
        res = 4294967296 - sm.get()
        if res < min_val[0]: min_val[0] = res
        deltas.append(res - min_val[0])

    if mode == STATE_PET:
        if utime.ticks_diff(now, last_tick) > 10000:
            hunger_lvl, boredom_lvl = max(0, hunger_lvl - 2), max(0, boredom_lvl - 3)
            last_tick = now
        
        if deltas[0] > 15000: hunger_lvl = min(100, hunger_lvl + 15); utime.sleep_ms(200)
        
        if deltas[5] > 15000: 
            mode = STATE_GAME
            cat_y, cat_v, obs_x, game_score = GROUND_Y, 0, 128, 0
            utime.sleep_ms(300)
        
        mood = "critical" if (hunger_lvl <= 0 or boredom_lvl <= 0) else "sad" if (hunger_lvl < 30 or boredom_lvl < 30) else "happy"
        draw_full_face(mood, blink=(now // 2000 % 3 == 0))
        
        if deltas[3] > 15000:
            oled.fill_rect(10, 0, 108, 20, 0); oled.rect(10, 0, 108, 20, 1)
            oled.text("H:{}% B:{}%".format(hunger_lvl, boredom_lvl), 18, 6)
            
    elif mode == STATE_GAME:
        oled.fill(0); oled.line(0, 56, 128, 56, 1)
        
        if deltas[5] > 10000 and cat_y >= GROUND_Y: cat_v = JUMP_VEL
        cat_v += GRAVITY
        cat_y += cat_v
        if cat_y > GROUND_Y: cat_y = GROUND_Y; cat_v = 0
        
        obs_x -= 8 #
        if obs_x < -16: obs_x = 128; game_score += 1
        
        # Animation:
        active_fb = fb_a if (cat_y < GROUND_Y or (now // 100 % 2 == 0)) else fb_b
        oled.blit(active_fb, 20, int(cat_y))
        
        oled.fill_rect(obs_x, 48, 8, 8, 1)
        oled.text("Win: {}/5".format(game_score), 35, 0)
        
        if obs_x < 52 and obs_x > 10 and cat_y > 32:
            led.pixels_fill((255, 0, 0)); led.pixels_show(); utime.sleep_ms(500); mode = STATE_PET
        if game_score >= 5:
            boredom_lvl = min(100, boredom_lvl + 30); mode = STATE_PET

    show_rotated(oled)
    utime.sleep_ms(5)