Community project

Lobster Tamagotchi Companion Bot

Эльдар Алиев

Published July 14, 2026 · Updated July 15, 2026

Raspberry Pi Pico11 components11 assembly steps
Remix this project

Build a playful lobster-themed virtual pet companion that runs on a Raspberry Pi Pico with battery power and a charming OLED display. This project combines a tactile interface with power monitoring, letting makers create an interactive desktop buddy that responds to button presses and displays mood states on a 128×64 yellow/blue screen.

The guide provides a complete wiring diagram for the power chain (18650 cell through TP4056 charger and boost converter), I2C/SPI display connections, button inputs, and status LED. Assembly steps walk through battery holder preparation, power rail integration with current sensing, and firmware setup to bring the bot to life with interactive behavior and battery awareness.

Wiring diagram

Interactive · read-only
Wiring diagram for Lobster Tamagotchi Companion Bot

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
Push Button1Momentary push button switch
Push Button1Momentary push button switch
Push Button1Momentary push button switch
Status LED (Red)1Single red LED with 330Ω series resistor for AI listening indicator.
Resistor 330Ω330 Ω1330 ohm current-limiting resistor for the status LED.
SSD1306 Yellow/Blue OLED 0.96" 128×64 SPI (7-pin)10.96 inch 128x64 two-color OLED. Top 16 rows are hardware-yellow, bottom 48 rows are hardware-blue. SSD1306 controller. 7-pin SPI interface: VCC, GND, D0 (CLK), D1 (MOSI), RES (RST), DC, CS.
Adafruit INA219 High-Side DC Current Sensor1INA219 high-side current and bus-voltage monitor breakout. It is powered from 3.3V or 5V and communicates over I2C. Route the measured load current through VIN+ and VIN-; those shunt terminals are part of the power path, not MCU GPIO.
TP4056 Li-Ion/LiPo charger module with protection1TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.
Boost Converter1Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral.
18650 Li-ion Cell118650 lithium-ion cell, nominal 3.7 V, ~2500 mAh. Common for higher-capacity portable / battery-bank style projects; needs a holder and protection / charger circuit.
18650 Holder1Generic holder for one or two removable 18650 Li-ion cells. It is a mechanical/electrical power holder, not a charger or protection circuit; pair with a charger/BMS and regulator appropriate to the cell count.

Assembly

11 steps
  1. Gather all parts

    Lay out: Raspberry Pi Zero 2W, SSD1306 yellow/blue OLED (SPI 7-pin), 3× push buttons, red LED, 330Ω resistor, Adafruit INA219 breakout, TP4056 Type-C module (with DW01 protection), MT3608 boost converter module, 18650 single-cell holder, 18650 Li-ion cell, breadboard or PCB, jumper wires.

    • Tip: Check the TP4056 module has TWO chips (TP4056 + DW01) — the dual-chip blue board includes over-discharge protection. Single-chip boards do NOT protect the cell.
    • Tip: Use the Adafruit INA219 breakout #904 — it has an onboard BSS138 level shifter safe for the Pi's 3.3V GPIO.
  2. Prepare the 18650 holder

    Insert the 18650 cell into the holder observing polarity — the positive terminal (+) of the cell goes to the spring-end marked (+) on the holder. The holder's red wire is BAT+, black wire is BAT-. Do NOT connect the holder wires to anything yet.

    • Tip: Most single-cell holders have a JST PH 2.0 connector or bare wire leads — check yours before soldering.
    • Tip: Mark the BAT+ wire with a small piece of tape if both wires are the same colour.
    • Never insert the 18650 cell backwards — reversed polarity will damage the TP4056 and may damage the cell.
    • Do not short BAT+ to BAT- — this will cause the cell to heat rapidly.
  3. Wire holder → TP4056 battery terminals

    Connect the 18650 holder's BAT+ (red) to the TP4056 B+ pad, and BAT- (black) to the TP4056 B- pad. These are the two pads labelled B+ / B- on the TP4056 module (not IN+ / IN-).

    • Tip: Solder directly to the TP4056 pads for a secure connection — push-in terminals on cheap modules can work loose.
    • Tip: Double-check B+ / B- vs IN+ / IN- — they are adjacent pads on most TP4056 modules.
    • Keep solder joints small and clean — B+ and B- pads are close together on the module.
  4. Wire TP4056 output → MT3608 boost converter

    Connect TP4056 OUT+ to MT3608 VIN+, and TP4056 OUT- to MT3608 VIN-. Then adjust the MT3608 boost output to exactly 5.0V by connecting a multimeter to VOUT+/VOUT- and turning the onboard trimmer potentiometer clockwise until the meter reads 5.00V. Do this BEFORE connecting the Pi.

    • Tip: Power the TP4056 via USB-C while adjusting the MT3608 trimmer — the cell will be charging during adjustment, which is fine.
    • Tip: 5.0V–5.1V output is ideal. Never exceed 5.25V into the Pi's GPIO rail.
    • Do NOT connect the Pi until the MT3608 output is confirmed at 5.0V. Over-voltage will permanently damage the Pi Zero 2W.
  5. Wire MT3608 → INA219 → Pi 5V rail

    Connect MT3608 VOUT+ to INA219 VIN+, and MT3608 VOUT- to INA219 VIN-. Then connect INA219 VIN- to Pi GPIO Pin 2 (5V rail) and INA219 GND to Pi Pin 6 (GND). This puts the INA219 shunt on the 5V supply line so it measures exactly what the Pi consumes.

    • Tip: The INA219 VIN+ / VIN- are the high-side shunt inputs — the 0.1Ω shunt resistor is onboard the Adafruit breakout.
    • Tip: Wire INA219 VCC (logic supply) to Pi 3.3V (Pin 1), not 5V.
  6. Wire INA219 I2C to Pi

    Connect INA219 SDA to Pi BCM2 (Pin 3) and INA219 SCL to Pi BCM3 (Pin 5). INA219 I2C address is 0x40 (both A0/A1 address pins floating = default).

    • Tip: BCM2 and BCM3 have 1.8kΩ pull-up resistors built into the Pi — no external pull-ups needed.
    • Tip: Confirm address 0x40 with: sudo i2cdetect -y 1 after booting the Pi.
  7. Wire OLED display (SPI)

    Connect the 7-pin yellow/blue SSD1306 OLED: GND→Pi Pin 6, VCC→Pi Pin 1 (3.3V), D0(CLK)→BCM11 (Pin 23), D1(MOSI)→BCM10 (Pin 19), CS→BCM8 (Pin 24), DC→BCM25 (Pin 22), RES→BCM27 (Pin 13).

    • Tip: The yellow zone is always the top ~16 rows regardless of software — design your layout to put the status bar there.
    • Tip: 3.3V ONLY on VCC — feeding 5V will destroy the OLED module.
    • Double-check D0=CLK and D1=MOSI — these are swapped on some module silkscreens.
  8. Wire the three buttons

    Each button has two pins: SIGNAL and GND. Connect: FEED button SIGNAL→BCM17 (Pin 11), PLAY button SIGNAL→BCM27 (Pin 13) — wait, BCM27 is used by OLED RES. PLAY→BCM22 (Pin 15), SLEEP button SIGNAL→BCM5 (Pin 29). Connect all button GND pins to Pi GND (Pin 6 or any GND pin). The Pi's internal pull-ups are enabled in firmware — no external resistors needed.

    • Tip: Use a common GND rail on your breadboard to keep wiring tidy.
  9. Wire status LED with resistor

    Connect BCM6 (Pin 31) → 330Ω resistor → LED anode (longer leg) → LED cathode (shorter leg) → GND. The resistor limits current to ~10mA which is safe for both the LED and the Pi GPIO.

    • Never connect an LED directly to a GPIO pin without a series resistor — it will draw too much current and may damage the GPIO.
  10. USB-C charging input

    Connect a USB-C cable from your power source (phone charger, power bank, PC port) to the TP4056 module's USB-C IN port. The blue LED on the TP4056 will light during charging; the red LED lights when fully charged. The Pi continues to run from the MT3608 boost output while the 18650 charges.

    • Tip: Any USB-C charger providing 5V/1A or more works. The TP4056 limits charge current to 1A by default (set by the PROG resistor on the module).
    • Tip: You can leave the USB-C plugged in indefinitely — the TP4056 + DW01 protection circuit handles overcharge cutoff automatically.
  11. Final checks and power-on

    Before powering on: (1) Confirm MT3608 output is 5.0V. (2) Confirm all GND connections share a common ground. (3) Confirm OLED VCC is on 3.3V, not 5V. (4) Confirm INA219 VCC is on 3.3V. (5) Insert the 18650 cell into the holder. (6) Connect USB-C to TP4056. The Pi should boot. SSH in and run: cd /home/pi/clawdbot && pip install -r data/requirements.txt && python src/main.py

    • Tip: If the Pi doesn't boot, re-check the MT3608 output voltage — it may have drifted during connection.
    • Tip: Run sudo i2cdetect -y 1 to confirm the INA219 shows up at address 0x40.
    • Never power the Pi from both the MT3608 rail AND the Micro-USB port simultaneously — this will back-feed current into the MT3608 and may damage the boost converter.

Pin assignments

Board wiring reference
PinConnectionType
EXTtp4056 IN+USB-C 5V charging inputpower
GNDtp4056 IN-ground
EXTbattery +V18650 Holder BAT+power
EXTbattery GND18650 Holder BAT-ground
EXTholder BAT+TP4056 Li-Ion/LiPo charger module with protection B+power
EXTholder BAT-TP4056 Li-Ion/LiPo charger module with protection B-ground
EXTtp4056 OUT+Boost Converter VIN+power
EXTtp4056 OUT-Boost Converter VIN-ground
EXTmt3608 VOUT+Adafruit INA219 High-Side DC Current Sensor VIN+power
EXTmt3608 VOUT-Adafruit INA219 High-Side DC Current Sensor VIN-ground
5Vina219 VIN-power
3V3ina219 VCCpower
GNDina219 GNDground
GPIO 2ina219 SDAi2c
GPIO 3ina219 SCLi2c
3V3oled VCCpower
GNDoled GNDground
GPIO 11oled D0spi
GPIO 10oled D1spi
GPIO 8oled CSspi
GPIO 25oled DCdigital
GPIO 27oled RESdigital
GPIO 17btn-feed SIGNALdigital
GNDbtn-feed GNDground
GPIO 22btn-play SIGNALdigital
GNDbtn-play GNDground
GPIO 5btn-sleep SIGNALdigital
GNDbtn-sleep GNDground
GPIO 6res-led Adigital
EXTres-led BStatus LED (Red) ANODEdigital
GNDstatus-led CATHODEground

Firmware

Raspberry Pi Pico
main.pyDeploy to device
#!/usr/bin/env python3
"""
ClawdBot Table Buddy — entry point
Raspberry Pi Zero 2W

Module layout
─────────────
  main.py        ← this file: boot, threads, glue
  src/state.py   ← BotState dataclass + mood logic
  src/sensors.py ← Wi-Fi RSSI + INA219 battery
  src/display.py ← SSD1306 Yellow/Blue OLED (luma.oled SPI)
  src/power.py   ← GPIO, buttons, LED blink
  src/bot.py     ← Telegram handlers, GPT-4o, reminders

Hardware
────────
  SSD1306 Yellow/Blue 0.96" 128×64 SPI (7-pin)
      D0 (CLK)  → BCM11   D1 (MOSI) → BCM10
      CS        → BCM8    DC        → BCM25   RES → BCM27
  INA219 (I2C)  SDA → BCM2   SCL → BCM3
  Buttons (active-low, pull-up)
      FEED → BCM17   PLAY → BCM22   SLEEP → BCM5
  Status LED → BCM6 → 330 Ω → GND
  Power chain: USB-C → TP4056 Type-C → 18650 (holder)
               TP4056 OUT → MT3608 boost (3.7→5V)
               MT3608 VOUT → INA219 VIN+ → Pi 5V (GPIO pin 2)
"""

import os
import sys
import time
import asyncio
import logging
import threading

from dotenv import load_dotenv
load_dotenv()

# ── Logging ───────────────────────────────────────────────────────────────────
logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    level=logging.INFO,
)
log = logging.getLogger("clawdbot.main")

# ── Environment ───────────────────────────────────────────────────────────────
TELEGRAM_TOKEN  = os.environ.get("TELEGRAM_TOKEN",  "")
OPENAI_API_KEY  = os.environ.get("OPENAI_API_KEY",  "")
ALLOWED_CHAT_ID = int(os.environ.get("ALLOWED_CHAT_ID", "0"))

if not TELEGRAM_TOKEN:
    log.error("TELEGRAM_TOKEN not set in .env — aborting.")
    sys.exit(1)

# ── Module imports (after env check) ─────────────────────────────────────────
from src.state   import state, STANDBY_TIMEOUT_S
from src.sensors import (init_ina219, get_wifi_rssi, rssi_to_bars,
                          get_battery_info)
from src.display import (oled, refresh, draw_active, draw_standby,
                          draw_hatch_step, _lock as _oled_lock)
from src.power   import gpio_setup, led_blink, gpio_cleanup
from src.bot     import init_bot

from PIL import Image

# ── Shared sensor cache (updated by sensor loop; read by bot.py) ──────────────
_last_wifi_bars: int  = 0
_last_bat:       dict = {"percent": -1, "charging": False, "available": False}


# ─────────────────────────────────────────────────────────────────────────────
# Hatch animation
# ─────────────────────────────────────────────────────────────────────────────
def hatch_animation():
    global _last_wifi_bars, _last_bat
    log.info("Hatch animation starting…")

    # Seed sensor readings for the status bar
    _last_wifi_bars = rssi_to_bars(get_wifi_rssi())
    _last_bat       = get_battery_info()

    steps = [
        (-1, "...zzzz..."),        # no egg yet
        (0,  "Something stirs!"),  # egg appears
        (1,  "*CRACK*"),           # first crack
        (2,  "I LIVE! SNAP!"),     # big crack → transitions to face
    ]

    for crack, label in steps:
        if crack == 2:
            # Final step: show the happy active face
            img = draw_active("happy", "I LIVE! SNAP!",
                              _last_wifi_bars, _last_bat)
            with _oled_lock:
                oled.display(img)
            led_blink(3, on_ms=200)
            time.sleep(2)
        else:
            img = draw_hatch_step(crack, label, _last_wifi_bars, _last_bat)
            with _oled_lock:
                oled.display(img)
            led_blink(1)
            time.sleep(1.5)

    state.hatched = True
    log.info("Hatch complete.")


# ─────────────────────────────────────────────────────────────────────────────
# Background threads
# ─────────────────────────────────────────────────────────────────────────────

# ── Sensor poll (Wi-Fi + battery, every 15 s) ─────────────────────────────────
def _sensor_loop():
    global _last_wifi_bars, _last_bat
    while True:
        time.sleep(15)
        rssi            = get_wifi_rssi()
        _last_wifi_bars = rssi_to_bars(rssi)
        _last_bat       = get_battery_info()
        refresh(state.mood, "", _last_wifi_bars, _last_bat)
        log.debug("Sensor poll → bars=%d bat_pct=%s",
                  _last_wifi_bars, _last_bat.get("percent"))


# ── Standby loop (inactivity check + blink animation) ─────────────────────────
def _standby_loop():
    BLINK_OPEN  = 2.7
    BLINK_CLOSE = 0.3
    blink_phase = True
    blink_timer = time.time()

    while True:
        time.sleep(0.1)
        idle = time.time() - state.last_activity

        if not state.standby:
            if idle >= STANDBY_TIMEOUT_S and state.hatched:
                state.standby = True
                blink_phase   = True
                blink_timer   = time.time()
                log.info("Standby: ENTER (idle=%.0fs)", idle)
                with _oled_lock:
                    try:
                        img = draw_standby(state.mood,
                                           _last_wifi_bars, _last_bat,
                                           blink_phase=True)
                        oled.display(img)
                    except Exception as exc:
                        log.warning("Standby enter error: %s", exc)
        else:
            elapsed   = time.time() - blink_timer
            threshold = BLINK_OPEN if blink_phase else BLINK_CLOSE
            if elapsed >= threshold:
                blink_phase = not blink_phase
                blink_timer = time.time()
                with _oled_lock:
                    try:
                        img = draw_standby(state.mood,
                                           _last_wifi_bars, _last_bat,
                                           blink_phase=blink_phase)
                        oled.display(img)
                    except Exception as exc:
                        log.warning("Standby blink error: %s", exc)


# ── Care loop (mood decay + Telegram nags, every 60 s) ────────────────────────
def _care_loop():
    from src.bot import send_cranky_nag, send_low_battery_alert
    global _last_bat
    nag_cooldown = 0
    lbo_alerted  = False

    while True:
        time.sleep(60)
        state.tick()
        _last_bat = get_battery_info()
        refresh(state.mood, "", _last_wifi_bars, _last_bat)
        led_blink(1)

        # Low-battery alert
        if _last_bat.get("low") and not lbo_alerted:
            lbo_alerted = True
            led_blink(5, on_ms=80, off_ms=80)
            send_low_battery_alert(_last_bat["voltage"])
        elif not _last_bat.get("low"):
            lbo_alerted = False

        # Charging log
        if _last_bat.get("charging"):
            log.info("TP4056: USB-C charging — %.2fV @ %.0fmA",
                     _last_bat["voltage"], _last_bat["current"])

        # Cranky nag
        if state.mood == "cranky" and nag_cooldown <= 0:
            send_cranky_nag()
            nag_cooldown = 10
        else:
            nag_cooldown = max(0, nag_cooldown - 1)


# ── Button poll (50 ms debounce) ───────────────────────────────────────────────
def _button_loop():
    import RPi.GPIO as GPIO
    from src.power import PIN_BTN_FEED, PIN_BTN_PLAY, PIN_BTN_SLEEP
    last = {PIN_BTN_FEED: 1, PIN_BTN_PLAY: 1, PIN_BTN_SLEEP: 1}
    actions = {
        PIN_BTN_FEED:  state.feed,
        PIN_BTN_PLAY:  state.play,
        PIN_BTN_SLEEP: state.sleep,
    }
    while True:
        time.sleep(0.05)
        for pin, action in actions.items():
            v = GPIO.input(pin)
            if v == 0 and last[pin] == 1:   # falling edge = press
                action()
                refresh(state.mood, "", _last_wifi_bars, _last_bat)
                led_blink(1)
                log.info("Button BCM%d → mood=%s", pin, state.mood)
            last[pin] = v


# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
async def main():
    loop = asyncio.get_running_loop()

    # Hardware init
    gpio_setup()
    init_ina219()
    hatch_animation()

    # Start background threads
    for target in (_sensor_loop, _standby_loop, _care_loop, _button_loop):
        threading.Thread(target=target, daemon=True).start()

    # Build and run the Telegram bot (blocks here via run_polling)
    app = init_bot(
        telegram_token  = TELEGRAM_TOKEN,
        openai_key      = OPENAI_API_KEY,
        allowed_chat_id = ALLOWED_CHAT_ID,
        loop            = loop,
    )

    log.info("ClawdBot online — polling Telegram…")
    refresh(state.mood, "Waiting for Telegram",
            _last_wifi_bars, _last_bat, force_active=True)

    await app.run_polling(allowed_updates=["message"])


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log.info("Shutting down.")
    finally:
        try:
            oled.cleanup()
        except Exception:
            pass
        gpio_cleanup()

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik