Community project
Lobster Tamagotchi Companion Bot
Эльдар Алиев
Published July 14, 2026 · Updated July 15, 2026
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
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| Push Button | 1 | Momentary push button switch |
| Push Button | 1 | Momentary push button switch |
| Push Button | 1 | Momentary push button switch |
| Status LED (Red) | 1 | Single red LED with 330Ω series resistor for AI listening indicator. |
| Resistor 330Ω330 Ω | 1 | 330 ohm current-limiting resistor for the status LED. |
| SSD1306 Yellow/Blue OLED 0.96" 128×64 SPI (7-pin) | 1 | 0.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 Sensor | 1 | INA219 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 protection | 1 | TP4056 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 Converter | 1 | Small 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 Cell | 1 | 18650 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 Holder | 1 | Generic 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 stepsGather 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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| Pin | Connection | Type |
|---|---|---|
| EXT | tp4056 IN+ → USB-C 5V charging input | power |
| GND | tp4056 IN- | ground |
| EXT | battery +V → 18650 Holder BAT+ | power |
| EXT | battery GND → 18650 Holder BAT- | ground |
| EXT | holder BAT+ → TP4056 Li-Ion/LiPo charger module with protection B+ | power |
| EXT | holder BAT- → TP4056 Li-Ion/LiPo charger module with protection B- | ground |
| EXT | tp4056 OUT+ → Boost Converter VIN+ | power |
| EXT | tp4056 OUT- → Boost Converter VIN- | ground |
| EXT | mt3608 VOUT+ → Adafruit INA219 High-Side DC Current Sensor VIN+ | power |
| EXT | mt3608 VOUT- → Adafruit INA219 High-Side DC Current Sensor VIN- | ground |
| 5V | ina219 VIN- | power |
| 3V3 | ina219 VCC | power |
| GND | ina219 GND | ground |
| GPIO 2 | ina219 SDA | i2c |
| GPIO 3 | ina219 SCL | i2c |
| 3V3 | oled VCC | power |
| GND | oled GND | ground |
| GPIO 11 | oled D0 | spi |
| GPIO 10 | oled D1 | spi |
| GPIO 8 | oled CS | spi |
| GPIO 25 | oled DC | digital |
| GPIO 27 | oled RES | digital |
| GPIO 17 | btn-feed SIGNAL | digital |
| GND | btn-feed GND | ground |
| GPIO 22 | btn-play SIGNAL | digital |
| GND | btn-play GND | ground |
| GPIO 5 | btn-sleep SIGNAL | digital |
| GND | btn-sleep GND | ground |
| GPIO 6 | res-led A | digital |
| EXT | res-led B → Status LED (Red) ANODE | digital |
| GND | status-led CATHODE | ground |
Firmware
Raspberry Pi Pico#!/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.