ESP32-C6 Desk Buddy
Animowany "biurkowy przyjaciel" na Seeed XIAO ESP32-C6 z czujnikiem gestów PAJ7620 i wyświetlaczem OLED 128×64. Reaguje na gesty emocjami, wykonuje webhooki HTTP, wyświetla informacje na żądanie, symuluje potrzeby w stylu Tamagotchi i pokazuje pogodę z Open-Meteo. Konfigurowany przez przeglądarkę.
Sprzęt
| Komponent | Model | Adres I2C |
|---|---|---|
| Mikrokontroler | Seeed XIAO ESP32-C6 | — |
| Wyświetlacz | SSD1306 OLED 128×64 | 0x3C |
| Czujnik gestów | CJMCU-7620 (PAJ7620U2) | 0x73 |
Podłączenie
Oba urządzenia dzielą jedną magistralę I2C (hardware, HP I2C bus 0):
SSD1306 / PAJ7620 XIAO ESP32-C6
──────────────────────────────────────
VCC → 3.3V
GND → GND
SDA → D4 (GPIO22)
SCL → D5 (GPIO23)
Ważne: ESP32-C6 ma dwa kontrolery I2C:
- I2C0 (HP,
Wire) — dowolne piny GPIO ✓- I2C1 (LP,
Wire1) — zablokowany sprzętowo na GPIO6/GPIO7, niedostępne na XIAO ✗Nie używaj
Wire1ani SW I2C (bit-bang blokuje WiFi na single-core CPU ~40 ms/frame).
Funkcje
Animacja oczu (Buddy)
Ciągła animacja na OLED ~20 fps. Buddy ma 12 nastrojów z różnymi kształtami oczu i ust:
| Nastrój | Wygląd | Opis |
|---|---|---|
normal |
pełne oczy, dryfujące źrenice | domyślny |
happy |
pełne oczy, uśmiech | wesołe policzki |
sleepy |
poziome kreski zZz |
bąbelki ZZZ, wolne mruganie |
surprised |
wielkie oczy o_O |
szeroko otwarte |
angry |
klinowe oczy >_< |
usta w dół |
sad |
dolna połowa oka T_T |
łzy, usta w górę |
excited |
gwiazdy *_* |
wzór × zamiast źrenic |
wink L |
;) | prawe oko otwarte, lewe zamknięte |
wink R |
(; | lewe oko otwarte, prawe zamknięte |
hungry |
zmniejszone oczy, usta otwarte | aktywowany przez głód |
playful |
iskrzące oczy | aktywowany przez nudę |
dirty |
rozognione źrenice | aktywowany przez potrzebę mycia |
Dodatkowe animacje:
- Mruganie — co 2,5–6 s (szybsze w trybie
sleepy) - Ruch źrenic — saccady do losowej pozycji co 1,5–4,5 s we wszystkich nastrojach; podczas snu wolny drift co 4–9 s
- Micro-tremor — drobne drżenie podczas fiksacji (tylko
normal/happy) - Auto-uśpienie — po 5 minutach bez gestu przechodzi w
sleepy - Nocne wygaszenie — między 00:00 a 05:00, po 5 minutach braku aktywności wyświetlacz gaśnie; wraca przy geście
Tamagotchi — potrzeby
Buddy ma trzy potrzeby narastające z czasem:
| Potrzeba | Kierunek | Tempo | Próg alarmu | Nastrój |
|---|---|---|---|---|
| Głód | 0=syt → 100=głodny | +1/2 min | ≥ 80 | hungry |
| Szczęście | 100=wesoły → 0=znudzony | −1/3 min | ≤ 20 | playful |
| Higiena | 100=czysty → 0=brudny | −1/4 min | ≤ 20 | dirty |
Gdy potrzeba przekroczy próg, Buddy automatycznie zmienia nastrój. W lewym dolnym rogu ekranu pojawiają się litery: G (głód), Z (zabawa), M (mycie).
Pogoda (Open-Meteo)
Co jakiś czas (konfigurowalny interwał) wyświetlany jest ekran pogodowy — zamiast oczu pojawia się ikona pogodowa, a zamiast ust — temperatura i ciśnienie:
| Ikona | Warunki |
|---|---|
| Słońce | Bezchmurnie (WMO 0) |
| Chmura + słońce | Częściowe zachmurzenie (WMO 1–2) |
| Chmura | Zachmurzenie, mgła (WMO 3–48) |
| Deszcz | Mżawka, deszcz, przelotny deszcz (WMO 51–82) |
| Śnieg | Śnieg i opady śnieżne (WMO 71–86) |
| Burza | Burza z piorunami (WMO 95–99) |
Dane z Open-Meteo — darmowe, bez klucza API. Dwuetapowe pobieranie: geocoding (nazwa miasta → współrzędne) + aktualna prognoza.
Gesty (PAJ7620)
Czujnik rozpoznaje 9 gestów ręką:
| Gest | Domyślna reakcja |
|---|---|
up |
happy |
down |
sad |
left |
surprised |
right |
surprised |
forward |
sleepy (trwały) |
backward |
angry |
clockwise |
excited |
anticlockwise |
normal |
wave |
excited |
Każdemu gestowi można przypisać własny nastrój, webhook i akcję przez panel konfiguracyjny.
Webhooki
Każdy gest może wywoływać webhook HTTP POST na skonfigurowany URL:
POST http://twoj-serwer/endpoint
Content-Type: application/json
{"gesture": "wave"}
- Wywołanie asynchroniczne (FreeRTOS task) — nie blokuje animacji
- Timeout 3 sekundy
Akcje
Akcje wyświetlają informacje lub reagują na potrzeby Buddy'ego:
| Akcja | Czas | Co robi |
|---|---|---|
| Data i godzina | 8 s | HH:MM:SS, DD.MM.RRRR, dzień tygodnia |
| Status WiFi | 8 s | SSID, IP, RSSI, słupki siły sygnału |
| Nakarm | 3 s | głód −30, nastrój happy |
| Pobaw się | 3 s | szczęście +25, nastrój excited |
| Umyj | 3 s | higiena +40, nastrój surprised |
| Status tamagotchi | 8 s | paski postępu: głód / szczęście / higiena |
Czas synchronizowany przez NTP (pool.ntp.org) — strefa CET/CEST (Polska).
Konfiguracja
Panel webowy
Po uruchomieniu wejdź przeglądarką na adres IP wypisany w Serial (lub na OLED przy starcie):
http://<IP>/
Panel zawiera cztery sekcje:
Gesty
Dla każdego gestu można ustawić URL webhoka, nastrój, akcję i czy gest jest aktywny.
Tamagotchi
Podgląd aktualnych potrzeb (głód, szczęście, higiena) z paskami postępu oraz przyciski: Nakarm, Pobaw się, Umyj.
Pogoda
Aktualny stan pogody (ikona, temperatura, ciśnienie) oraz ustawienia:
- Miasto — nazwa w języku polskim lub angielskim (np.
Warszawa,Kraków) - Co ile sekund — interwał wyświetlania ekranu pogodowego (0 = wyłączony)
- Pokazuj przez (s) — czas trwania ekranu pogodowego
Test nastroju
12 przycisków do ręcznego ustawienia dowolnego nastroju na 10 sekund — do testowania wyglądu oczu bez czujnika gestów.
JSON API
# Odczyt / zapis konfiguracji gestów
GET http://<IP>/api/config
POST http://<IP>/api/config (Content-Type: application/json)
# Stan Tamagotchi
GET http://<IP>/api/tama
# Akcje Tamagotchi
POST http://<IP>/api/tama/feed
POST http://<IP>/api/tama/play
POST http://<IP>/api/tama/clean
# Aktualna pogoda + konfiguracja
GET http://<IP>/api/weather
# Zapis konfiguracji pogody (form POST)
POST http://<IP>/weather/save
# Test nastroju (0–11) na 10 sekund
POST http://<IP>/api/mood/test?m=1
Przykład zapisu gestów:
{
"wave": {
"url": "http://homeassistant.local:8123/api/webhook/my_hook",
"mood": 6,
"action": 0,
"enabled": true
},
"up": {
"url": "",
"mood": 0,
"action": 3,
"enabled": true
}
}
Wartości mood
| Wartość | Nastrój |
|---|---|
| 0 | bez zmiany (domyślna reakcja gestu) |
| 1 | happy |
| 2 | sleepy |
| 3 | surprised |
| 4 | angry |
| 5 | sad |
| 6 | excited |
| 7 | wink L |
| 8 | wink R |
| 9 | hungry |
| 10 | playful |
| 11 | dirty |
Wartości action
| Wartość | Akcja |
|---|---|
| 0 | brak |
| 1 | data i godzina |
| 2 | status WiFi |
| 3 | nakarm (głód −30) |
| 4 | pobaw się (szczęście +25) |
| 5 | umyj (higiena +40) |
| 6 | status tamagotchi |
Konfiguracja WiFi
Dane sieci przechowywane są w pliku data/wifi.json na LittleFS. Plik nie jest commitowany i nigdy nie jest serwowany przez HTTP — hasło WiFi pozostaje tylko na urządzeniu.
{
"ssid": "twoja_siec",
"password": "twoje_haslo"
}
Konfiguracja gestów
Plik data/config.json zawiera wyłącznie konfigurację gestów (bez danych WiFi). Jest dostępny przez GET /config.json i pobieralny komendą task config-download.
{
"up": { "url": "", "mood": 0, "action": 3, "enabled": true },
"wave": { "url": "http://ha.local/webhook/x", "mood": 6, "action": 0, "enabled": true }
}
Konfiguracja pogody
Konfiguracja przechowywana w /weather.json na LittleFS — zapisywana przez panel webowy, persystuje przez restarty. Nie wymaga reflashowania.
Instalacja i build
Wymagania
- PlatformIO (CLI lub VS Code extension)
- Task (
brew install go-task)
Pierwsze uruchomienie
# Konfiguracja WiFi
cp data/wifi.json.example data/wifi.json
nano data/wifi.json # uzupełnij ssid i password
# Konfiguracja gestów (opcjonalnie — domyślne wartości działają od razu)
cp data/config.json.example data/config.json
task flash-monitor # uploadfs + upload + monitor
Typowe komendy
| Komenda | Opis |
|---|---|
task build |
Tylko kompilacja |
task flash |
Wgraj filesystem + firmware |
task upload |
Wgraj tylko firmware (zachowuje LittleFS) |
task uploadfs |
Wgraj data/wifi.json + data/config.json |
task monitor |
Monitor portu szeregowego |
task flash-monitor |
Pełne wgranie + monitor |
task wifi |
Edytuj WiFi (data/wifi.json) + wgraj FS |
task gestures |
Edytuj gesty (data/config.json) + wgraj FS |
task config-download |
Pobierz konfigurację gestów z urządzenia |
task erase-flash |
Skasuj flash i wgraj od nowa |
task test |
Uruchom testy jednostkowe (native, na Mac) |
task |
Lista wszystkich zadań |
Uwaga:
task uploadfsnadpisuje LittleFS — konfiguracja pogody zapisana przez panel webowy zostanie utracona. Użyjtask uploaddo aktualizacji samego firmware'u.
Zmiana WiFi bez rekompilacji
task wifi # otwiera edytor + automatycznie uploadfs
Zależności (pobierane automatycznie)
olikraus/U8g2
acrandal/RevEng PAJ7620
bblanchon/ArduinoJson@^7.2.1
mathieucarbou/ESPAsyncWebServer@^3.3.12
Architektura kodu
Logika domenowa wydzielona do bibliotek w lib/ — zero zależności od Arduino, kompilowalne natywnie.
src/main.cpp — hardware + HTTP server + WiFi + FreeRTOS (~1100 linii)
lib/
BuddyDomain/ — BuddyTypes.h, BuddyLogic.h/.cpp (nastroje, mruganie, źrenice)
TamaLogic/ — TamaLogic.h/.cpp (potrzeby, ticki, override nastroju)
GestureConfig/ — GestureConfig.h/.cpp (struct, enums, tablice stringów)
test/native/
test_buddy/ — ~21 testów Unity (initBuddy, mruganie, saccady)
test_tama/ — ~19 testów Unity (ticki, progi, tamaFeed/Play/Clean)
data/
wifi.json — dane WiFi (nie commitowane)
config.json — konfiguracja gestów
partitions.csv — własna tablica partycji (app 1.75 MB, LittleFS 2.19 MB)
Przepływ inicjalizacji
setup()
loadAllConfig()
/wifi.json → WIFI_SSID/PASS (nigdy nie serwowane)
/config.json → gConfig[] (gesty)
/weather.json → weatherCfg (miasto, interwał)
initBuddy() + initTama()
Wire.begin(22, 23) ← I2C0 HP dla obu urządzeń
u8g2.begin() ← SSD1306 HW I2C
sensor.begin(&Wire) ← PAJ7620 HW I2C
connectWiFi() → NTP sync
setupHttpServer() ← AsyncWebServer port 80
loop()
sensor.readGesture() ← co 500 ms
handleGesture()
executeAction() ← efekt tama + overlay OLED
setBuddyMood() ← skonfigurowany lub domyślny
fireWebhook() ← FreeRTOS task (async)
updateTama() ← co 10 s (potrzeby + mood override)
updateBuddyAnim() ← co 50 ms (mruganie, saccady, ZZZ)
showBuddyScreen() ← co 50 ms (~20 fps)
drawWeatherFace() ← priorytet gdy weatherShowUntil aktywny
Kluczowe decyzje techniczne
| Problem | Rozwiązanie |
|---|---|
| SW I2C blokuje WiFi 40ms/frame na single-core ESP32-C6 | HW I2C (Wire) dla obu urządzeń na wspólnej magistrali |
WebServer.h nie działa na ESP32-C6 z IDF 5.x |
ESPAsyncWebServer (callback-based, brak handleClient()) |
Wire1 (LP I2C) zablokowany na GPIO6/7 (niedostępne na XIAO) |
Jeden Wire (HP I2C) dla SSD1306 + PAJ7620 |
setContrast() ma zbyt wąski zakres wizualny na SSD1306 |
setPowerSave(1/0) — komenda 0xAE/0xAF wyłącza panel |
| Hasło WiFi dostępne przez HTTP | Oddzielny /wifi.json — nigdy nie serwowany; /config.json zawiera tylko gesty |
| Firmware nie mieścił się w partycji OTA (1.28 MB) | Własna partitions.csv: app0 1.75 MB, LittleFS 2.19 MB |
| Domena logiki — testy natywne bez sprzętu | Biblioteki w lib/ bez zależności Arduino; pio test -e native |
Testy
task test # pio test -e native — 40 testów na Mac, bez urządzenia
Testy pokrywają: inicjalizację stanu, cykl mrugania (OPEN→CLOSING→CLOSED→OPENING), saccady źrenic, ticki tamagotchi, progi nastrojów, tamaFeed/Play/Clean z wartościami brzegowymi.
Dodawanie nowych akcji
- Dodaj wartość do
enum ActionwGestureConfig.h - Zaktualizuj
NUM_ACTIONSiACTION_LABELS[]wGestureConfig.cpp - Napisz funkcję
showXxxScreen() - Dodaj
case ACTION_XXX:wshowBuddyScreen()(sekcja overlay) - Opcjonalnie: dodaj efekt tama w
executeAction()(np. zmiana potrzeby)