feat: moods, emotions, needs
This commit is contained in:
@@ -34,3 +34,10 @@ credentials.h
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# WiFi credentials — never commit
|
||||||
|
data/wifi.json
|
||||||
|
|
||||||
|
# Gesture config — may contain webhook URLs, keep local
|
||||||
|
data/config.json
|
||||||
|
|
||||||
|
|||||||
@@ -164,13 +164,19 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### Konfiguracja WiFi
|
### Konfiguracja WiFi
|
||||||
|
|
||||||
Dane sieci są na razie hardcoded w `src/main.cpp`:
|
Dane sieci przechowywane są w pliku `data/config.json` na LittleFS (osobny obszar flash). Plik **nie jest commitowany do repozytorium** (`.gitignore`).
|
||||||
|
|
||||||
```cpp
|
```json
|
||||||
const char *WIFI_SSID = "twoja_siec";
|
{
|
||||||
const char *WIFI_PASS = "twoje_haslo";
|
"wifi": {
|
||||||
|
"ssid": "twoja_siec",
|
||||||
|
"password": "twoje_haslo"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Plik wgrywany jest osobnym poleceniem (`task uploadfs`) — zmiana WiFi nie wymaga rekompilacji firmware.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Instalacja i build
|
## Instalacja i build
|
||||||
@@ -178,13 +184,36 @@ const char *WIFI_PASS = "twoje_haslo";
|
|||||||
### Wymagania
|
### Wymagania
|
||||||
|
|
||||||
- [PlatformIO](https://platformio.org/) (CLI lub VS Code extension)
|
- [PlatformIO](https://platformio.org/) (CLI lub VS Code extension)
|
||||||
|
- [Task](https://taskfile.dev/) (`brew install go-task`)
|
||||||
|
|
||||||
### Build i upload
|
### Pierwsze uruchomienie
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd esp32-c6
|
cp data/config.json.example data/config.json # lub edytuj ręcznie
|
||||||
pio run --target upload
|
# uzupełnij ssid i password w data/config.json
|
||||||
pio device monitor
|
|
||||||
|
task flash-monitor # uploadfs + upload + monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typowe komendy
|
||||||
|
|
||||||
|
| Komenda | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| `task build` | Tylko kompilacja |
|
||||||
|
| `task flash` | Wgraj filesystem + firmware |
|
||||||
|
| `task upload` | Wgraj tylko firmware |
|
||||||
|
| `task uploadfs` | Wgraj tylko `data/config.json` |
|
||||||
|
| `task monitor` | Monitor portu szeregowego |
|
||||||
|
| `task flash-monitor` | Pełne wgranie + monitor |
|
||||||
|
| `task config-upload` | Edytuj config WiFi + wgraj FS |
|
||||||
|
| `task erase-flash` | Skasuj flash i wgraj od nowa |
|
||||||
|
| `task` | Lista wszystkich zadań |
|
||||||
|
|
||||||
|
### Zmiana WiFi bez rekompilacji
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edytuj data/config.json, potem:
|
||||||
|
task uploadfs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zależności (pobierane automatycznie)
|
### Zależności (pobierane automatycznie)
|
||||||
@@ -196,29 +225,15 @@ bblanchon/ArduinoJson@^7.2.1
|
|||||||
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
||||||
```
|
```
|
||||||
|
|
||||||
### platformio.ini
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[env:esp32-c6]
|
|
||||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
|
|
||||||
board = seeed_xiao_esp32c6
|
|
||||||
framework = arduino
|
|
||||||
lib_deps =
|
|
||||||
olikraus/U8g2
|
|
||||||
acrandal/RevEng PAJ7620
|
|
||||||
bblanchon/ArduinoJson@^7.2.1
|
|
||||||
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
|
||||||
build_flags = -std=gnu++17
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architektura kodu
|
## Architektura kodu
|
||||||
|
|
||||||
Cały projekt mieści się w jednym pliku `src/main.cpp` (~650 linii).
|
Cały projekt mieści się w jednym pliku `src/main.cpp` (~700 linii).
|
||||||
|
|
||||||
```
|
```
|
||||||
setup()
|
setup()
|
||||||
|
loadWiFiConfig() ← LittleFS /config.json → WIFI_SSID/PASS
|
||||||
Wire.begin(22, 23) ← I2C0 HP dla obu urządzeń
|
Wire.begin(22, 23) ← I2C0 HP dla obu urządzeń
|
||||||
u8g2.begin() ← SSD1306 HW I2C
|
u8g2.begin() ← SSD1306 HW I2C
|
||||||
sensor.begin(&Wire) ← PAJ7620 HW I2C
|
sensor.begin(&Wire) ← PAJ7620 HW I2C
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
ENV: esp32-c6
|
||||||
|
PORT: /dev/cu.usbmodem1101
|
||||||
|
BAUD: "115200"
|
||||||
|
DEVICE_IP: "" # ustaw np.: task config-download DEVICE_IP=192.168.1.42
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Kompiluj firmware
|
||||||
|
cmds:
|
||||||
|
- pio run -e {{.ENV}}
|
||||||
|
|
||||||
|
upload:
|
||||||
|
desc: Wgraj firmware na urzadzenie
|
||||||
|
cmds:
|
||||||
|
- pio run -e {{.ENV}} --target upload
|
||||||
|
|
||||||
|
uploadfs:
|
||||||
|
desc: Wgraj filesystem (data/wifi.json + data/config.json) na urzadzenie
|
||||||
|
cmds:
|
||||||
|
- pio run -e {{.ENV}} --target uploadfs
|
||||||
|
|
||||||
|
flash:
|
||||||
|
desc: Wgraj filesystem i firmware (pelne wgranie)
|
||||||
|
cmds:
|
||||||
|
- task: uploadfs
|
||||||
|
- task: upload
|
||||||
|
|
||||||
|
monitor:
|
||||||
|
desc: Otworz monitor portu szeregowego
|
||||||
|
cmds:
|
||||||
|
- pio device monitor -p {{.PORT}} -b {{.BAUD}}
|
||||||
|
|
||||||
|
upload-monitor:
|
||||||
|
desc: Wgraj firmware i otworz monitor
|
||||||
|
cmds:
|
||||||
|
- task: upload
|
||||||
|
- task: monitor
|
||||||
|
|
||||||
|
flash-monitor:
|
||||||
|
desc: Pelne wgranie (fs + firmware) i otworz monitor
|
||||||
|
cmds:
|
||||||
|
- task: flash
|
||||||
|
- task: monitor
|
||||||
|
|
||||||
|
erase:
|
||||||
|
desc: Skasuj cala pamiec flash urzadzenia
|
||||||
|
cmds:
|
||||||
|
- esptool.py --port {{.PORT}} erase_flash
|
||||||
|
|
||||||
|
erase-flash:
|
||||||
|
desc: Skasuj flash, wgraj fs i firmware od nowa
|
||||||
|
cmds:
|
||||||
|
- task: erase
|
||||||
|
- task: flash
|
||||||
|
|
||||||
|
clean:
|
||||||
|
desc: Wyczysc katalog build
|
||||||
|
cmds:
|
||||||
|
- pio run -e {{.ENV}} --target clean
|
||||||
|
|
||||||
|
deps:
|
||||||
|
desc: Zainstaluj/zaktualizuj biblioteki
|
||||||
|
cmds:
|
||||||
|
- pio pkg install -e {{.ENV}}
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
desc: Edytuj konfiguracje WiFi (data/wifi.json) i wgraj filesystem
|
||||||
|
cmds:
|
||||||
|
- ${EDITOR:-nano} data/wifi.json
|
||||||
|
- task: uploadfs
|
||||||
|
|
||||||
|
gestures:
|
||||||
|
desc: Edytuj konfiguracje gestow (data/config.json) i wgraj filesystem
|
||||||
|
cmds:
|
||||||
|
- ${EDITOR:-nano} data/config.json
|
||||||
|
- task: uploadfs
|
||||||
|
|
||||||
|
config-download:
|
||||||
|
desc: "Pobierz konfiguracje gestow z urzadzenia -> data/config.json (uzyj: task config-download DEVICE_IP=<ip>)"
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
IP="{{.DEVICE_IP}}"
|
||||||
|
if [ -z "$IP" ]; then
|
||||||
|
read -rp "IP urzadzenia: " IP
|
||||||
|
fi
|
||||||
|
echo "[config-download] Pobieranie z http://$IP/config.json ..."
|
||||||
|
curl -sf "http://$IP/config.json" -o data/config.json \
|
||||||
|
&& echo "[config-download] Zapisano do data/config.json (tylko gesty, bez WiFi)" \
|
||||||
|
|| { echo "[config-download] BLAD: nie mozna polaczyc z $IP"; exit 1; }
|
||||||
|
|
||||||
|
default:
|
||||||
|
desc: Pokaz liste dostepnych zadan
|
||||||
|
cmds:
|
||||||
|
- task --list
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"up": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"down": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"left": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"right": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"forward": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"backward": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"clockwise": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"anticlockwise": { "url": "", "mood": 0, "action": 0, "enabled": true },
|
||||||
|
"wave": { "url": "", "mood": 0, "action": 0, "enabled": true }
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"ssid": "twoja_siec",
|
||||||
|
"password": "twoje_haslo"
|
||||||
|
}
|
||||||
@@ -13,3 +13,5 @@ lib_deps =
|
|||||||
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
||||||
build_flags =
|
build_flags =
|
||||||
-std=gnu++17
|
-std=gnu++17
|
||||||
|
-D FORMAT_LITTLEFS_IF_FAILED
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
|||||||
+582
-164
@@ -4,17 +4,19 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <Preferences.h>
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <RevEng_PAJ7620.h>
|
#include <RevEng_PAJ7620.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
// ── Hardware ──────────────────────────────────────────────────────────────────
|
// ── Hardware ──────────────────────────────────────────────────────────────────
|
||||||
// Both SSD1306 (0x3C) and PAJ7620 (0x73) share Wire on GPIO22(SDA)/GPIO23(SCL)
|
// Both SSD1306 (0x3C) and PAJ7620 (0x73) share Wire on GPIO22(SDA)/GPIO23(SCL)
|
||||||
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 23, 22);
|
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 23, 22);
|
||||||
RevEng_PAJ7620 sensor;
|
RevEng_PAJ7620 sensor;
|
||||||
|
|
||||||
const char *WIFI_SSID = "SSID";
|
// WiFi credentials — loaded from LittleFS /config.json at boot
|
||||||
const char *WIFI_PASS = "PASSWORD";
|
static char WIFI_SSID[64] = "";
|
||||||
|
static char WIFI_PASS[64] = "";
|
||||||
|
|
||||||
|
|
||||||
// ── Gesture config ─────────────────────────────────────────────────────────────
|
// ── Gesture config ─────────────────────────────────────────────────────────────
|
||||||
// Index 0-8 maps to GES_UP..GES_WAVE (Gesture enum value - 1)
|
// Index 0-8 maps to GES_UP..GES_WAVE (Gesture enum value - 1)
|
||||||
@@ -29,17 +31,24 @@ static const char *GKEY[NUM_GESTURES] = {
|
|||||||
// Mood labels matching Mood enum values
|
// Mood labels matching Mood enum values
|
||||||
static const char *MOOD_LABELS[] = {
|
static const char *MOOD_LABELS[] = {
|
||||||
"-- bez zmiany --", "happy ^_^", "sleepy zZz", "surprised o_O",
|
"-- bez zmiany --", "happy ^_^", "sleepy zZz", "surprised o_O",
|
||||||
"angry >_<", "sad T_T", "excited *_*"};
|
"angry >_<", "sad T_T", "excited *_*", "wink L ;)", "wink R (;",
|
||||||
|
"hungry :(", "playful :D", "dirty ..."};
|
||||||
|
|
||||||
// Actions that can be triggered by a gesture
|
// Actions that can be triggered by a gesture
|
||||||
enum Action
|
enum Action
|
||||||
{
|
{
|
||||||
ACTION_NONE = 0,
|
ACTION_NONE = 0,
|
||||||
ACTION_DATETIME = 1,
|
ACTION_DATETIME = 1,
|
||||||
ACTION_WIFI = 2
|
ACTION_WIFI = 2,
|
||||||
|
ACTION_FEED = 3, // nakarm — zmniejsza głód
|
||||||
|
ACTION_PLAY = 4, // pobaw się — zwiększa szczęście
|
||||||
|
ACTION_CLEAN = 5, // umyj — zwiększa higienę
|
||||||
|
ACTION_STATUS = 6 // pokaż status potrzeb
|
||||||
};
|
};
|
||||||
static const uint8_t NUM_ACTIONS = 3;
|
static const uint8_t NUM_ACTIONS = 7;
|
||||||
static const char *ACTION_LABELS[] = {"-- brak --", "Data i godzina", "Status WiFi"};
|
static const char *ACTION_LABELS[] = {
|
||||||
|
"-- brak --", "Data i godzina", "Status WiFi",
|
||||||
|
"Nakarm", "Pobaw sie", "Umyj", "Status tamagotchi"};
|
||||||
|
|
||||||
struct GestureConfig
|
struct GestureConfig
|
||||||
{
|
{
|
||||||
@@ -50,7 +59,6 @@ struct GestureConfig
|
|||||||
};
|
};
|
||||||
|
|
||||||
GestureConfig gConfig[NUM_GESTURES];
|
GestureConfig gConfig[NUM_GESTURES];
|
||||||
Preferences prefs;
|
|
||||||
AsyncWebServer httpServer(80);
|
AsyncWebServer httpServer(80);
|
||||||
|
|
||||||
// ── Buddy ─────────────────────────────────────────────────────────────────────
|
// ── Buddy ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -62,7 +70,12 @@ enum Mood
|
|||||||
MOOD_SURPRISED,
|
MOOD_SURPRISED,
|
||||||
MOOD_ANGRY,
|
MOOD_ANGRY,
|
||||||
MOOD_SAD,
|
MOOD_SAD,
|
||||||
MOOD_EXCITED
|
MOOD_EXCITED,
|
||||||
|
MOOD_WINK_L, // lewe oko zamknięte
|
||||||
|
MOOD_WINK_R, // prawe oko zamknięte
|
||||||
|
MOOD_HUNGRY, // głodny — chce jeść
|
||||||
|
MOOD_PLAYFUL, // chce się bawić
|
||||||
|
MOOD_DIRTY // potrzeba mycia
|
||||||
};
|
};
|
||||||
enum BlinkState
|
enum BlinkState
|
||||||
{
|
{
|
||||||
@@ -74,10 +87,12 @@ enum BlinkState
|
|||||||
|
|
||||||
static const uint8_t EYE_L_X = 38;
|
static const uint8_t EYE_L_X = 38;
|
||||||
static const uint8_t EYE_R_X = 90;
|
static const uint8_t EYE_R_X = 90;
|
||||||
static const uint8_t EYE_Y = 30;
|
static const uint8_t EYE_Y = 27;
|
||||||
static const uint8_t EYE_RX = 17;
|
static const uint8_t EYE_RX = 17;
|
||||||
static const uint8_t EYE_RY = 15;
|
static const uint8_t EYE_RY = 15;
|
||||||
static const uint8_t PUPIL_R = 6;
|
static const uint8_t PUPIL_R = 6;
|
||||||
|
static const uint8_t MOUTH_X = 64;
|
||||||
|
static const uint8_t MOUTH_Y = 52;
|
||||||
|
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
@@ -93,12 +108,46 @@ struct
|
|||||||
uint32_t nextLook;
|
uint32_t nextLook;
|
||||||
uint8_t zzzPhase;
|
uint8_t zzzPhase;
|
||||||
uint32_t nextZzz;
|
uint32_t nextZzz;
|
||||||
|
uint32_t nextMicroTremor; // involuntary fixation tremor
|
||||||
} buddy;
|
} buddy;
|
||||||
|
|
||||||
|
// ── Tamagotchi needs ──────────────────────────────────────────────────────────
|
||||||
|
// hunger 0=full→100=starving (++/2min), happiness 100=happy→0=bored (--/3min),
|
||||||
|
// hygiene 100=clean→0=dirty (--/4min). Threshold 70 triggers mood override.
|
||||||
|
struct {
|
||||||
|
uint8_t hunger;
|
||||||
|
uint8_t happiness;
|
||||||
|
uint8_t hygiene;
|
||||||
|
uint32_t nextHungerTick;
|
||||||
|
uint32_t nextHappyTick;
|
||||||
|
uint32_t nextHygieneTick;
|
||||||
|
} tama;
|
||||||
|
|
||||||
|
void initTama() {
|
||||||
|
tama.hunger = 10;
|
||||||
|
tama.happiness = 80;
|
||||||
|
tama.hygiene = 90;
|
||||||
|
uint32_t now = millis();
|
||||||
|
tama.nextHungerTick = now + 120000UL;
|
||||||
|
tama.nextHappyTick = now + 180000UL;
|
||||||
|
tama.nextHygieneTick = now + 240000UL;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Action overlay ────────────────────────────────────────────────────────────
|
// ── Action overlay ────────────────────────────────────────────────────────────
|
||||||
uint32_t overlayUntil = 0; // show overlay until this timestamp
|
uint32_t overlayUntil = 0; // show overlay until this timestamp
|
||||||
Action overlayAction = ACTION_NONE;
|
Action overlayAction = ACTION_NONE;
|
||||||
|
|
||||||
|
// ── Night dim ─────────────────────────────────────────────────────────────────
|
||||||
|
// 00:00–05:00, after 5 s idle → display off (SSD1306 0xAE). Gesture restores.
|
||||||
|
// setContrast() is unreliable on SSD1306 (visual range too narrow);
|
||||||
|
// setPowerSave(1) sends the display-off command directly.
|
||||||
|
static bool g_dimmed = false;
|
||||||
|
static void setDim(bool dim) {
|
||||||
|
if (g_dimmed == dim) return;
|
||||||
|
g_dimmed = dim;
|
||||||
|
u8g2.setPowerSave(dim ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
void setBuddyMood(Mood m, uint32_t durationMs = 0)
|
void setBuddyMood(Mood m, uint32_t durationMs = 0)
|
||||||
{
|
{
|
||||||
buddy.mood = m;
|
buddy.mood = m;
|
||||||
@@ -140,107 +189,244 @@ void initBuddy()
|
|||||||
buddy.nextBlink = millis() + 3000;
|
buddy.nextBlink = millis() + 3000;
|
||||||
buddy.nextLook = millis() + 2000;
|
buddy.nextLook = millis() + 2000;
|
||||||
buddy.nextZzz = millis() + 3000;
|
buddy.nextZzz = millis() + 3000;
|
||||||
|
buddy.nextMicroTremor = millis() + 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Eye drawing ───────────────────────────────────────────────────────────────
|
// ── Eye drawing ───────────────────────────────────────────────────────────────
|
||||||
|
// Manga style: white sclera + heavy top lid + large dark iris + highlights.
|
||||||
|
// effRy drives the blink animation (compresses the eye vertically).
|
||||||
|
// pdx/pdy shift the iris for gaze tracking (NORMAL/HAPPY).
|
||||||
|
static void drawWinkEye(uint8_t cx, uint8_t cy) {
|
||||||
|
// Closed crescent — upper arc of a circle centered below the eye
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.drawCircle(cx, cy + 8, 10, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
|
u8g2.drawCircle(cx, cy + 8, 11, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||||
int8_t pdx, int8_t pdy, bool isLeft)
|
int8_t pdx, int8_t pdy, bool isLeft)
|
||||||
{
|
{
|
||||||
if (effRy == 0)
|
// Buddy faces the viewer, so buddy's LEFT eye = screen RIGHT (!isLeft)
|
||||||
return;
|
if (buddy.mood == MOOD_WINK_L && !isLeft) { drawWinkEye(cx, cy); return; }
|
||||||
|
if (buddy.mood == MOOD_WINK_R && isLeft) { drawWinkEye(cx, cy); return; }
|
||||||
|
if (effRy == 0) return;
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
|
||||||
|
switch (buddy.mood)
|
||||||
|
{
|
||||||
|
case MOOD_NORMAL:
|
||||||
|
case MOOD_HAPPY: {
|
||||||
|
// Filled ellipse + drifting specular glint
|
||||||
|
uint8_t ry = min(effRy, EYE_RX);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, EYE_RX, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 5) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
int8_t gx = (int8_t)cx + 3 + pdx / 3;
|
||||||
|
int8_t gy = (int8_t)cy - 3 + pdy / 3;
|
||||||
|
u8g2.drawDisc((uint8_t)gx, (uint8_t)gy, 2);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_SLEEPY: {
|
||||||
|
// Thick horizontal dash; height shrinks with effRy for blink
|
||||||
|
uint8_t h = (effRy >= 3) ? 4 : effRy;
|
||||||
|
u8g2.drawBox(cx - EYE_RX, cy - h / 2, EYE_RX * 2, h ? h : 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_SURPRISED: {
|
||||||
|
// Wider ellipse + larger glint
|
||||||
|
uint8_t r = EYE_RX + 2;
|
||||||
|
uint8_t ry = min((uint8_t)(effRy + 2), r);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, r, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 6) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawDisc(cx + 5, cy - 5, 3);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_ANGRY: {
|
||||||
|
// Angular wedge: full width outer-bottom, diagonal cut inner-top
|
||||||
|
const uint8_t w = EYE_RX + 3;
|
||||||
|
const uint8_t h = min(effRy, (uint8_t)6);
|
||||||
|
for (int8_t row = 0; row <= (int8_t)(h * 2); row++) {
|
||||||
|
int8_t y = (int8_t)cy - (int8_t)h + row;
|
||||||
|
uint8_t lineW = (row < (int8_t)h) ? w - (uint8_t)((int8_t)h - row) : w;
|
||||||
|
if (lineW < 2) lineW = 2;
|
||||||
|
uint8_t x = isLeft ? cx - w : (uint8_t)(cx + w - lineW);
|
||||||
|
u8g2.drawHLine(x, (uint8_t)y, lineW);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_SAD: {
|
||||||
|
// Droopy: lower half of ellipse only
|
||||||
|
uint8_t ry = min(effRy, EYE_RX);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy - 2, EYE_RX, ry,
|
||||||
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_EXCITED: {
|
||||||
|
// Shining disc with X-star cutout
|
||||||
|
uint8_t ry = min(effRy, EYE_RX);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, EYE_RX, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 5) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawLine(cx - 5, cy - 5, cx + 5, cy + 5);
|
||||||
|
u8g2.drawLine(cx + 5, cy - 5, cx - 5, cy + 5);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.drawDisc(cx, cy, 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_HUNGRY: {
|
||||||
|
// Worried eyes — smaller, looking down toward imaginary food
|
||||||
|
uint8_t ry = min(effRy, (uint8_t)(EYE_RX - 2));
|
||||||
|
u8g2.drawFilledEllipse(cx, cy + 2, EYE_RX - 2, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 4) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawDisc(cx, cy + 4, 2);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_PLAYFUL: {
|
||||||
|
// Wide sparkling eyes — lively double glint
|
||||||
|
uint8_t ry = min((uint8_t)(effRy + 1), (uint8_t)(EYE_RX + 1));
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, EYE_RX, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 5) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawDisc(cx + 4, cy - 4, 3);
|
||||||
|
u8g2.drawDisc(cx - 3, cy + 3, 1);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MOOD_DIRTY: {
|
||||||
|
// Dizzy/disgusted — two offset pupils, unfocused look
|
||||||
|
uint8_t ry = min(effRy, EYE_RX);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, EYE_RX, ry, U8G2_DRAW_ALL);
|
||||||
|
if (ry >= 5) {
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawDisc(cx - 4, cy - 2, 2);
|
||||||
|
u8g2.drawDisc(cx + 4, cy + 2, 2);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
uint8_t ry = min(effRy, EYE_RY);
|
||||||
|
u8g2.drawFilledEllipse(cx, cy, EYE_RX, ry, U8G2_DRAW_ALL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mouth drawing ─────────────────────────────────────────────────────────────
|
||||||
|
// Coordinate notes (Y increases downward):
|
||||||
|
// drawCircle(..., LOWER) → arc opens downward = smile ✓
|
||||||
|
// drawCircle(..., UPPER) → arc opens upward = frown ✓
|
||||||
|
static void drawMouth()
|
||||||
|
{
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
switch (buddy.mood)
|
switch (buddy.mood)
|
||||||
{
|
{
|
||||||
|
|
||||||
case MOOD_HAPPY:
|
case MOOD_HAPPY:
|
||||||
u8g2.setDrawColor(1);
|
// Wide "U" smile + round cheek blush dots
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 7, 10,
|
||||||
u8g2.setDrawColor(0);
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
u8g2.drawBox(cx - EYE_RX - 1, cy - effRy - 1, EYE_RX * 2 + 3, effRy + 2);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 7, 11,
|
||||||
u8g2.setDrawColor(1);
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
u8g2.drawDisc(MOUTH_X - 20, MOUTH_Y, 3);
|
||||||
|
u8g2.drawDisc(MOUTH_X + 20, MOUTH_Y, 3);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOOD_SLEEPY:
|
case MOOD_SLEEPY:
|
||||||
u8g2.setDrawColor(1);
|
// Thick horizontal bar — tired, barely open
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
u8g2.drawBox(MOUTH_X - 8, MOUTH_Y - 1, 16, 3);
|
||||||
u8g2.setDrawColor(0);
|
|
||||||
u8g2.drawBox(cx - EYE_RX - 1, cy - effRy - 1, EYE_RX * 2 + 3, (effRy * 3) / 2 + 1);
|
|
||||||
u8g2.setDrawColor(1);
|
|
||||||
if (effRy > 3)
|
|
||||||
{
|
|
||||||
u8g2.setDrawColor(0);
|
|
||||||
u8g2.drawDisc(cx + pdx, cy + effRy - 3, (uint8_t)(PUPIL_R - 3));
|
|
||||||
u8g2.setDrawColor(1);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOOD_SURPRISED:
|
case MOOD_SURPRISED:
|
||||||
u8g2.setDrawColor(1);
|
// Open "O" — filled ring (outline + hollow)
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX + 2, effRy, U8G2_DRAW_ALL);
|
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 5, 6, U8G2_DRAW_ALL);
|
||||||
u8g2.setDrawColor(0);
|
u8g2.setDrawColor(0);
|
||||||
u8g2.drawDisc(cx, cy, (uint8_t)(PUPIL_R - 2));
|
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 3, 4, U8G2_DRAW_ALL);
|
||||||
u8g2.setDrawColor(1);
|
u8g2.setDrawColor(1);
|
||||||
u8g2.drawDisc(cx - 3, cy - 2, 2);
|
|
||||||
{
|
|
||||||
int8_t s = isLeft ? -2 : 2;
|
|
||||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 6, cx + EYE_RX - 2, cy - effRy - 6 + s);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOOD_ANGRY:
|
case MOOD_ANGRY:
|
||||||
u8g2.setDrawColor(1);
|
// Tight frown with lower lip bar
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 7, 7,
|
||||||
u8g2.setDrawColor(0);
|
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
u8g2.drawDisc(cx + pdx, cy + pdy, PUPIL_R);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 7, 8,
|
||||||
u8g2.setDrawColor(1);
|
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 2);
|
u8g2.drawBox(MOUTH_X - 7, MOUTH_Y + 6, 14, 2);
|
||||||
if (isLeft)
|
|
||||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 4, cx + EYE_RX - 2, cy - effRy - 9);
|
|
||||||
else
|
|
||||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 9, cx + EYE_RX - 2, cy - effRy - 4);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOOD_SAD:
|
case MOOD_SAD:
|
||||||
u8g2.setDrawColor(1);
|
// Deep frown — teardrops on cheeks
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 9, 9,
|
||||||
u8g2.setDrawColor(0);
|
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
u8g2.drawDisc(cx + pdx, cy + pdy, PUPIL_R);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 9, 10,
|
||||||
u8g2.setDrawColor(1);
|
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 2);
|
// teardrops
|
||||||
if (isLeft)
|
u8g2.drawDisc(MOUTH_X - 18, MOUTH_Y + 5, 2);
|
||||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 9, cx + EYE_RX - 2, cy - effRy - 4);
|
u8g2.drawLine(MOUTH_X - 18, MOUTH_Y + 7, MOUTH_X - 18, MOUTH_Y + 11);
|
||||||
else
|
u8g2.drawDisc(MOUTH_X + 18, MOUTH_Y + 5, 2);
|
||||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 4, cx + EYE_RX - 2, cy - effRy - 9);
|
u8g2.drawLine(MOUTH_X + 18, MOUTH_Y + 7, MOUTH_X + 18, MOUTH_Y + 11);
|
||||||
u8g2.drawLine(cx + (isLeft ? 4 : -4), cy + effRy,
|
|
||||||
cx + (isLeft ? 4 : -4), cy + effRy + 7);
|
|
||||||
u8g2.drawDisc(cx + (isLeft ? 4 : -4), cy + effRy + 8, 2);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOOD_EXCITED:
|
case MOOD_EXCITED:
|
||||||
u8g2.setDrawColor(1);
|
// Wide open "D" mouth — flat top, arc bottom
|
||||||
u8g2.drawCircle(cx, cy, EYE_RX - 1, U8G2_DRAW_ALL);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 6, 11,
|
||||||
u8g2.drawLine(cx - 9, cy - 9, cx + 9, cy + 9);
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
u8g2.drawLine(cx + 9, cy - 9, cx - 9, cy + 9);
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 6, 12,
|
||||||
u8g2.drawLine(cx - 11, cy, cx + 11, cy);
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
u8g2.drawLine(cx, cy - 11, cx, cy + 11);
|
u8g2.drawBox(MOUTH_X - 12, MOUTH_Y - 7, 24, 2);
|
||||||
u8g2.drawDisc(cx, cy, 3);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
case MOOD_HUNGRY:
|
||||||
u8g2.setDrawColor(1);
|
// Open oval mouth — anticipating food
|
||||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 5, 4, U8G2_DRAW_ALL);
|
||||||
if (effRy > 3)
|
|
||||||
{
|
|
||||||
int8_t pr = (effRy >= PUPIL_R) ? (int8_t)PUPIL_R : (int8_t)(effRy - 1);
|
|
||||||
u8g2.setDrawColor(0);
|
u8g2.setDrawColor(0);
|
||||||
u8g2.drawDisc(cx + pdx, cy + pdy, (uint8_t)pr);
|
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 3, 2, U8G2_DRAW_ALL);
|
||||||
if (pr >= 3)
|
|
||||||
{
|
|
||||||
u8g2.setDrawColor(1);
|
|
||||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u8g2.setDrawColor(1);
|
u8g2.setDrawColor(1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MOOD_PLAYFUL:
|
||||||
|
// Extra-wide smile
|
||||||
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 8, 12,
|
||||||
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 8, 13,
|
||||||
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MOOD_DIRTY:
|
||||||
|
// Wavy nauseous line
|
||||||
|
u8g2.drawDisc(MOUTH_X - 8, MOUTH_Y + 1, 1);
|
||||||
|
u8g2.drawDisc(MOUTH_X - 4, MOUTH_Y - 1, 1);
|
||||||
|
u8g2.drawDisc(MOUTH_X, MOUTH_Y + 2, 1);
|
||||||
|
u8g2.drawDisc(MOUTH_X + 4, MOUTH_Y - 1, 1);
|
||||||
|
u8g2.drawDisc(MOUTH_X + 8, MOUTH_Y + 1, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // MOOD_NORMAL + winks
|
||||||
|
// Small gentle smile
|
||||||
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 3, 6,
|
||||||
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 3, 7,
|
||||||
|
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +515,89 @@ static void showDateTimeScreen()
|
|||||||
u8g2.sendBuffer();
|
u8g2.sendBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tamagotchi action screens ──────────────────────────────────────────────────
|
||||||
|
static void showFeedScreen() {
|
||||||
|
u8g2.clearBuffer();
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.setFont(u8g2_font_7x13_tr);
|
||||||
|
u8g2.drawStr(28, 14, "Mniam!");
|
||||||
|
// plate
|
||||||
|
u8g2.drawEllipse(64, 44, 18, 7, U8G2_DRAW_ALL);
|
||||||
|
// food on plate
|
||||||
|
u8g2.drawFilledEllipse(64, 40, 10, 5, U8G2_DRAW_ALL);
|
||||||
|
// steam lines
|
||||||
|
u8g2.drawLine(57, 30, 55, 22);
|
||||||
|
u8g2.drawLine(64, 30, 64, 22);
|
||||||
|
u8g2.drawLine(71, 30, 73, 22);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showPlayScreen() {
|
||||||
|
u8g2.clearBuffer();
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.setFont(u8g2_font_7x13_tr);
|
||||||
|
u8g2.drawStr(16, 14, "Grajmy!");
|
||||||
|
// ball
|
||||||
|
u8g2.drawDisc(44, 42, 10);
|
||||||
|
// star burst lines
|
||||||
|
for (int8_t a = 0; a < 8; a++) {
|
||||||
|
float rad = a * 3.14159f / 4.0f;
|
||||||
|
u8g2.drawLine(80, 38,
|
||||||
|
(uint8_t)(80 + 13 * cos(rad)),
|
||||||
|
(uint8_t)(38 + 13 * sin(rad)));
|
||||||
|
}
|
||||||
|
u8g2.drawDisc(80, 38, 7);
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawStr(74, 42, "!");
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showCleanScreen() {
|
||||||
|
u8g2.clearBuffer();
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.setFont(u8g2_font_7x13_tr);
|
||||||
|
u8g2.drawStr(24, 14, "Mycie!");
|
||||||
|
// water drops
|
||||||
|
for (uint8_t i = 0; i < 5; i++) {
|
||||||
|
uint8_t dx = 30 + i * 16;
|
||||||
|
u8g2.drawDisc(dx, 44, 5);
|
||||||
|
u8g2.drawLine(dx, 36, dx, 38);
|
||||||
|
u8g2.drawLine(dx - 2, 37, dx + 2, 37);
|
||||||
|
}
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showTamaStatusScreen() {
|
||||||
|
u8g2.clearBuffer();
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
u8g2.setFont(u8g2_font_5x7_tr);
|
||||||
|
|
||||||
|
auto drawBar = [](uint8_t y, const char *lbl, uint8_t val) {
|
||||||
|
u8g2.drawStr(0, y, lbl);
|
||||||
|
u8g2.drawFrame(36, y - 7, 74, 8);
|
||||||
|
u8g2.drawBox(36, y - 7, (uint8_t)(val * 74 / 100), 8);
|
||||||
|
};
|
||||||
|
|
||||||
|
drawBar(11, "Glod:", tama.hunger); // bar = how hungry (fill=bad)
|
||||||
|
// invert happiness/hygiene bars: fill = good
|
||||||
|
u8g2.drawStr(0, 27, "Rad:");
|
||||||
|
u8g2.drawFrame(36, 20, 74, 8);
|
||||||
|
u8g2.drawBox(36, 20, (uint8_t)(tama.happiness * 74 / 100), 8);
|
||||||
|
|
||||||
|
u8g2.drawStr(0, 43, "Czyst:");
|
||||||
|
u8g2.drawFrame(36, 36, 74, 8);
|
||||||
|
u8g2.drawBox(36, 36, (uint8_t)(tama.hygiene * 74 / 100), 8);
|
||||||
|
|
||||||
|
// value labels
|
||||||
|
char buf[5];
|
||||||
|
snprintf(buf, sizeof(buf), "%3d%%", tama.hunger); u8g2.drawStr(112, 11, buf);
|
||||||
|
snprintf(buf, sizeof(buf), "%3d%%", tama.happiness); u8g2.drawStr(112, 27, buf);
|
||||||
|
snprintf(buf, sizeof(buf), "%3d%%", tama.hygiene); u8g2.drawStr(112, 43, buf);
|
||||||
|
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
void showBuddyScreen()
|
void showBuddyScreen()
|
||||||
{
|
{
|
||||||
// Action overlay takes priority
|
// Action overlay takes priority
|
||||||
@@ -336,14 +605,13 @@ void showBuddyScreen()
|
|||||||
{
|
{
|
||||||
switch (overlayAction)
|
switch (overlayAction)
|
||||||
{
|
{
|
||||||
case ACTION_DATETIME:
|
case ACTION_DATETIME: showDateTimeScreen(); break;
|
||||||
showDateTimeScreen();
|
case ACTION_WIFI: showWiFiStatusScreen(); break;
|
||||||
break;
|
case ACTION_FEED: showFeedScreen(); break;
|
||||||
case ACTION_WIFI:
|
case ACTION_PLAY: showPlayScreen(); break;
|
||||||
showWiFiStatusScreen();
|
case ACTION_CLEAN: showCleanScreen(); break;
|
||||||
break;
|
case ACTION_STATUS: showTamaStatusScreen(); break;
|
||||||
default:
|
default: break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -353,30 +621,70 @@ void showBuddyScreen()
|
|||||||
u8g2.setDrawColor(1);
|
u8g2.setDrawColor(1);
|
||||||
|
|
||||||
uint8_t effRy = buddy.blinkRy;
|
uint8_t effRy = buddy.blinkRy;
|
||||||
if (buddy.mood == MOOD_HAPPY)
|
// SURPRISED: eyes open wider than normal max
|
||||||
effRy = min(effRy, (uint8_t)13);
|
|
||||||
if (buddy.mood == MOOD_SLEEPY)
|
|
||||||
effRy = min(effRy, (uint8_t)11);
|
|
||||||
if (buddy.mood == MOOD_SURPRISED)
|
if (buddy.mood == MOOD_SURPRISED)
|
||||||
effRy = EYE_RY + 3;
|
effRy = min((uint8_t)(EYE_RY + 2), (uint8_t)(buddy.blinkRy + 2));
|
||||||
|
|
||||||
drawEye(EYE_L_X, EYE_Y, effRy, buddy.pupilDx, buddy.pupilDy, true);
|
drawEye(EYE_L_X, EYE_Y, effRy, buddy.pupilDx, buddy.pupilDy, true);
|
||||||
drawEye(EYE_R_X, EYE_Y, effRy, -buddy.pupilDx, buddy.pupilDy, false);
|
drawEye(EYE_R_X, EYE_Y, effRy, -buddy.pupilDx, buddy.pupilDy, false);
|
||||||
|
|
||||||
const char *labels[] = {"", "^_^", "zZz", "o_O", ">_<", "T_T", "*_*"};
|
drawMouth();
|
||||||
u8g2.setFont(u8g2_font_5x7_tr);
|
|
||||||
uint8_t lw = u8g2.getStrWidth(labels[buddy.mood]);
|
|
||||||
u8g2.drawStr((128 - lw) / 2, 63, labels[buddy.mood]);
|
|
||||||
|
|
||||||
if (buddy.mood == MOOD_SLEEPY && buddy.zzzPhase > 0)
|
if (buddy.mood == MOOD_SLEEPY && buddy.zzzPhase > 0)
|
||||||
{
|
{
|
||||||
const char *zStr[] = {"z", "zz", "zzz"};
|
const char *zStr[] = {"z", "zz", "zzz"};
|
||||||
uint8_t zi = buddy.zzzPhase - 1;
|
uint8_t zi = buddy.zzzPhase - 1;
|
||||||
u8g2.drawStr(EYE_R_X + EYE_RX + 2 + zi * 2, EYE_Y - EYE_RY - 3 - zi * 5, zStr[zi]);
|
u8g2.setFont(u8g2_font_5x7_tr);
|
||||||
|
u8g2.drawStr(EYE_R_X + EYE_RX + 3 + zi * 2,
|
||||||
|
EYE_Y - EYE_RY - 2 - zi * 4, zStr[zi]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tama need indicators — small icons bottom-left when threshold exceeded
|
||||||
|
{
|
||||||
|
u8g2.setFont(u8g2_font_4x6_tr);
|
||||||
|
uint8_t ix = 1;
|
||||||
|
if (tama.hunger >= 70) { u8g2.drawStr(ix, 63, "G"); ix += 6; }
|
||||||
|
if (tama.happiness <= 30) { u8g2.drawStr(ix, 63, "Z"); ix += 6; }
|
||||||
|
if (tama.hygiene <= 30) { u8g2.drawStr(ix, 63, "M"); }
|
||||||
|
}
|
||||||
|
|
||||||
u8g2.sendBuffer();
|
u8g2.sendBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateTama()
|
||||||
|
{
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
// Tick needs over time
|
||||||
|
if (now >= tama.nextHungerTick) {
|
||||||
|
tama.nextHungerTick = now + 120000UL;
|
||||||
|
if (tama.hunger < 100) tama.hunger++;
|
||||||
|
}
|
||||||
|
if (now >= tama.nextHappyTick) {
|
||||||
|
tama.nextHappyTick = now + 180000UL;
|
||||||
|
if (tama.happiness > 0) tama.happiness--;
|
||||||
|
}
|
||||||
|
if (now >= tama.nextHygieneTick) {
|
||||||
|
tama.nextHygieneTick = now + 240000UL;
|
||||||
|
if (tama.hygiene > 0) tama.hygiene--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override buddy mood based on needs (only when no temp mood + not in sleep)
|
||||||
|
// Direct assignment avoids resetting lastEvent
|
||||||
|
if (buddy.revertAt == 0 && buddy.mood != MOOD_SLEEPY &&
|
||||||
|
buddy.mood != MOOD_WINK_L && buddy.mood != MOOD_WINK_R) {
|
||||||
|
if (tama.hunger >= 80)
|
||||||
|
buddy.mood = MOOD_HUNGRY;
|
||||||
|
else if (tama.hygiene <= 20)
|
||||||
|
buddy.mood = MOOD_DIRTY;
|
||||||
|
else if (tama.happiness <= 20)
|
||||||
|
buddy.mood = MOOD_PLAYFUL;
|
||||||
|
else if (buddy.mood == MOOD_HUNGRY || buddy.mood == MOOD_DIRTY ||
|
||||||
|
buddy.mood == MOOD_PLAYFUL)
|
||||||
|
buddy.mood = MOOD_NORMAL; // needs satisfied → back to normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void updateBuddyAnim()
|
void updateBuddyAnim()
|
||||||
{
|
{
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
@@ -389,7 +697,8 @@ void updateBuddyAnim()
|
|||||||
if (buddy.mood == MOOD_NORMAL && now - buddy.lastEvent > 300000UL)
|
if (buddy.mood == MOOD_NORMAL && now - buddy.lastEvent > 300000UL)
|
||||||
setBuddyMood(MOOD_SLEEPY, 0);
|
setBuddyMood(MOOD_SLEEPY, 0);
|
||||||
|
|
||||||
if (buddy.mood != MOOD_SURPRISED && buddy.mood != MOOD_EXCITED)
|
if (buddy.mood != MOOD_SURPRISED && buddy.mood != MOOD_EXCITED &&
|
||||||
|
buddy.mood != MOOD_WINK_L && buddy.mood != MOOD_WINK_R)
|
||||||
{
|
{
|
||||||
switch (buddy.blinkState)
|
switch (buddy.blinkState)
|
||||||
{
|
{
|
||||||
@@ -427,20 +736,44 @@ void updateBuddyAnim()
|
|||||||
buddy.blinkRy = EYE_RY;
|
buddy.blinkRy = EYE_RY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buddy.mood == MOOD_NORMAL && now >= buddy.nextLook)
|
// Saccadic movement — fast when far (3 px/tick), slow when close (1 px/tick)
|
||||||
{
|
{
|
||||||
buddy.pupilTargetDx = (int8_t)random(-6, 7);
|
int8_t ddx = buddy.pupilTargetDx - buddy.pupilDx;
|
||||||
buddy.pupilTargetDy = (int8_t)random(-4, 5);
|
int8_t ddy = buddy.pupilTargetDy - buddy.pupilDy;
|
||||||
buddy.nextLook = now + random(1500, 4000);
|
if (ddx) buddy.pupilDx += (ddx > 0 ? 1 : -1) * ((abs(ddx) >= 4) ? 3 : 1);
|
||||||
|
if (ddy) buddy.pupilDy += (ddy > 0 ? 1 : -1) * ((abs(ddy) >= 4) ? 3 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gaze — random saccades + micro-tremor for expressive moods
|
||||||
|
{
|
||||||
|
bool fixated = (buddy.pupilDx == buddy.pupilTargetDx &&
|
||||||
|
buddy.pupilDy == buddy.pupilTargetDy);
|
||||||
|
|
||||||
|
if (buddy.mood == MOOD_SLEEPY) {
|
||||||
|
// Slow drowsy drift — limited range, eyes mostly down
|
||||||
|
if (fixated && now >= buddy.nextLook) {
|
||||||
|
buddy.pupilTargetDx = (int8_t)random(-3, 4);
|
||||||
|
buddy.pupilTargetDy = (int8_t)random(2, 6);
|
||||||
|
buddy.nextLook = now + random(4000, 9000);
|
||||||
|
}
|
||||||
|
} else if (buddy.mood != MOOD_WINK_L && buddy.mood != MOOD_WINK_R) {
|
||||||
|
// Active gaze — full range saccades
|
||||||
|
if (fixated && now >= buddy.nextLook) {
|
||||||
|
buddy.pupilTargetDx = (int8_t)random(-7, 8);
|
||||||
|
buddy.pupilTargetDy = (int8_t)random(-5, 6);
|
||||||
|
buddy.nextLook = now + random(1500, 4500);
|
||||||
|
}
|
||||||
|
// Micro-tremor while fixated (NORMAL / HAPPY only)
|
||||||
|
if ((buddy.mood == MOOD_NORMAL || buddy.mood == MOOD_HAPPY) &&
|
||||||
|
fixated && now >= buddy.nextMicroTremor) {
|
||||||
|
buddy.pupilTargetDx = (int8_t)constrain(
|
||||||
|
buddy.pupilTargetDx + (int8_t)random(-1, 2), -7, 7);
|
||||||
|
buddy.pupilTargetDy = (int8_t)constrain(
|
||||||
|
buddy.pupilTargetDy + (int8_t)random(-1, 2), -5, 5);
|
||||||
|
buddy.nextMicroTremor = now + random(300, 700);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (buddy.pupilDx < buddy.pupilTargetDx)
|
|
||||||
buddy.pupilDx++;
|
|
||||||
else if (buddy.pupilDx > buddy.pupilTargetDx)
|
|
||||||
buddy.pupilDx--;
|
|
||||||
if (buddy.pupilDy < buddy.pupilTargetDy)
|
|
||||||
buddy.pupilDy++;
|
|
||||||
else if (buddy.pupilDy > buddy.pupilTargetDy)
|
|
||||||
buddy.pupilDy--;
|
|
||||||
|
|
||||||
if (buddy.mood == MOOD_SLEEPY && now >= buddy.nextZzz)
|
if (buddy.mood == MOOD_SLEEPY && now >= buddy.nextZzz)
|
||||||
{
|
{
|
||||||
@@ -449,51 +782,77 @@ void updateBuddyAnim()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Config persistence (NVS via Preferences) ──────────────────────────────────
|
// ── Config persistence ─────────────────────────────────────────────────────────
|
||||||
void loadConfig()
|
// WiFi: /wifi.json — credentials, NEVER served over HTTP
|
||||||
|
// Gestures:/config.json — gesture config, served via GET /config.json
|
||||||
|
static void loadJsonFile(const char *path, JsonDocument &doc)
|
||||||
{
|
{
|
||||||
prefs.begin("buddy", false); // false = read-write, creates namespace if missing
|
File f = LittleFS.open(path, "r");
|
||||||
for (uint8_t i = 0; i < NUM_GESTURES; i++)
|
if (!f) { Serial.printf("[Config] %s not found\n", path); return; }
|
||||||
{
|
if (deserializeJson(doc, f) != DeserializationError::Ok)
|
||||||
char key[16];
|
Serial.printf("[Config] %s parse error\n", path);
|
||||||
|
f.close();
|
||||||
snprintf(key, sizeof(key), "wh.%s.url", GKEY[i]);
|
|
||||||
String url = prefs.getString(key, "");
|
|
||||||
strncpy(gConfig[i].url, url.c_str(), sizeof(gConfig[i].url) - 1);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.mood", GKEY[i]);
|
|
||||||
gConfig[i].mood = (uint8_t)prefs.getUInt(key, 0);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.en", GKEY[i]);
|
|
||||||
gConfig[i].enabled = prefs.getBool(key, true);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.act", GKEY[i]);
|
|
||||||
gConfig[i].action = (uint8_t)prefs.getUInt(key, 0);
|
|
||||||
}
|
|
||||||
prefs.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void loadAllConfig()
|
||||||
|
{
|
||||||
|
if (!LittleFS.begin(true)) {
|
||||||
|
Serial.println("[Config] LittleFS mount failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi from /wifi.json { "ssid": "...", "password": "..." }
|
||||||
|
{
|
||||||
|
JsonDocument wdoc;
|
||||||
|
loadJsonFile("/wifi.json", wdoc);
|
||||||
|
strncpy(WIFI_SSID, wdoc["ssid"] | "", sizeof(WIFI_SSID) - 1);
|
||||||
|
strncpy(WIFI_PASS, wdoc["password"] | "", sizeof(WIFI_PASS) - 1);
|
||||||
|
Serial.printf("[Config] WiFi SSID: %s\n", WIFI_SSID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestures from /config.json { "up": {...}, "down": {...}, ... }
|
||||||
|
{
|
||||||
|
JsonDocument doc;
|
||||||
|
loadJsonFile("/config.json", doc);
|
||||||
|
for (uint8_t i = 0; i < NUM_GESTURES; i++) {
|
||||||
|
JsonObject g = doc[GNAME[i]];
|
||||||
|
if (g.isNull()) continue;
|
||||||
|
strncpy(gConfig[i].url, g["url"] | "", sizeof(gConfig[i].url) - 1);
|
||||||
|
gConfig[i].mood = g["mood"] | 0;
|
||||||
|
gConfig[i].action = g["action"] | 0;
|
||||||
|
gConfig[i].enabled = g["enabled"] | true;
|
||||||
|
}
|
||||||
|
Serial.println("[Config] Gestures loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
LittleFS.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves ONLY gesture config — WiFi credentials are never written here
|
||||||
void saveConfig()
|
void saveConfig()
|
||||||
{
|
{
|
||||||
prefs.begin("buddy", false); // read-write
|
JsonDocument doc;
|
||||||
for (uint8_t i = 0; i < NUM_GESTURES; i++)
|
for (uint8_t i = 0; i < NUM_GESTURES; i++) {
|
||||||
{
|
doc[GNAME[i]]["url"] = gConfig[i].url;
|
||||||
char key[16];
|
doc[GNAME[i]]["mood"] = gConfig[i].mood;
|
||||||
|
doc[GNAME[i]]["action"] = gConfig[i].action;
|
||||||
snprintf(key, sizeof(key), "wh.%s.url", GKEY[i]);
|
doc[GNAME[i]]["enabled"] = gConfig[i].enabled;
|
||||||
prefs.putString(key, gConfig[i].url);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.mood", GKEY[i]);
|
|
||||||
prefs.putUInt(key, gConfig[i].mood);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.en", GKEY[i]);
|
|
||||||
prefs.putBool(key, gConfig[i].enabled);
|
|
||||||
|
|
||||||
snprintf(key, sizeof(key), "wh.%s.act", GKEY[i]);
|
|
||||||
prefs.putUInt(key, gConfig[i].action);
|
|
||||||
}
|
}
|
||||||
prefs.end();
|
|
||||||
Serial.println("[Config] Saved to NVS");
|
if (!LittleFS.begin(true)) {
|
||||||
|
Serial.println("[Config] LittleFS mount failed — save aborted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File f = LittleFS.open("/config.json", "w");
|
||||||
|
if (!f) {
|
||||||
|
Serial.println("[Config] Cannot open /config.json for writing");
|
||||||
|
LittleFS.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serializeJsonPretty(doc, f);
|
||||||
|
f.close();
|
||||||
|
LittleFS.end();
|
||||||
|
Serial.println("[Config] Gestures saved to /config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Async webhook via FreeRTOS task ───────────────────────────────────────────
|
// ── Async webhook via FreeRTOS task ───────────────────────────────────────────
|
||||||
@@ -529,10 +888,6 @@ void fireWebhook(uint8_t gestureIdx)
|
|||||||
if (!cfg.enabled || strlen(cfg.url) == 0)
|
if (!cfg.enabled || strlen(cfg.url) == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Set mood before firing (immediate feedback)
|
|
||||||
if (cfg.mood > 0)
|
|
||||||
setBuddyMood((Mood)cfg.mood, 4000);
|
|
||||||
|
|
||||||
// Fire async
|
// Fire async
|
||||||
WebhookTask *t = new WebhookTask;
|
WebhookTask *t = new WebhookTask;
|
||||||
strncpy(t->url, cfg.url, sizeof(t->url) - 1);
|
strncpy(t->url, cfg.url, sizeof(t->url) - 1);
|
||||||
@@ -590,7 +945,7 @@ static String buildHtml()
|
|||||||
html += "'></td><td><select name='mood_";
|
html += "'></td><td><select name='mood_";
|
||||||
html += GKEY[i];
|
html += GKEY[i];
|
||||||
html += "'>";
|
html += "'>";
|
||||||
for (uint8_t m = 0; m < 7; m++)
|
for (uint8_t m = 0; m < 12; m++)
|
||||||
{
|
{
|
||||||
html += "<option value='";
|
html += "<option value='";
|
||||||
html += m;
|
html += m;
|
||||||
@@ -700,6 +1055,24 @@ void setupHttpServer()
|
|||||||
saveConfig();
|
saveConfig();
|
||||||
req->send(200, "application/json", "{\"ok\":true}"); });
|
req->send(200, "application/json", "{\"ok\":true}"); });
|
||||||
|
|
||||||
|
// GET /config.json — gesture config only (WiFi credentials are NEVER exposed)
|
||||||
|
httpServer.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *req) {
|
||||||
|
if (!LittleFS.begin(false)) {
|
||||||
|
req->send(500, "application/json", "{\"error\":\"LittleFS unavailable\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File f = LittleFS.open("/config.json", "r");
|
||||||
|
if (!f) {
|
||||||
|
LittleFS.end();
|
||||||
|
req->send(404, "application/json", "{\"error\":\"not found\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String body = f.readString();
|
||||||
|
f.close();
|
||||||
|
LittleFS.end();
|
||||||
|
req->send(200, "application/json", body);
|
||||||
|
});
|
||||||
|
|
||||||
httpServer.begin();
|
httpServer.begin();
|
||||||
Serial.printf("[HTTP] Server na http://%s/\n", WiFi.localIP().toString().c_str());
|
Serial.printf("[HTTP] Server na http://%s/\n", WiFi.localIP().toString().c_str());
|
||||||
}
|
}
|
||||||
@@ -769,9 +1142,33 @@ void executeAction(uint8_t idx)
|
|||||||
Action a = (Action)gConfig[idx].action;
|
Action a = (Action)gConfig[idx].action;
|
||||||
if (a == ACTION_NONE)
|
if (a == ACTION_NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
uint32_t dur = 8000;
|
||||||
|
switch (a) {
|
||||||
|
case ACTION_FEED:
|
||||||
|
tama.hunger = (tama.hunger >= 30) ? tama.hunger - 30 : 0;
|
||||||
|
setBuddyMood(MOOD_HAPPY, 4000);
|
||||||
|
dur = 3000;
|
||||||
|
break;
|
||||||
|
case ACTION_PLAY:
|
||||||
|
tama.happiness = (uint8_t)min((int)tama.happiness + 25, 100);
|
||||||
|
setBuddyMood(MOOD_EXCITED, 4000);
|
||||||
|
dur = 3000;
|
||||||
|
break;
|
||||||
|
case ACTION_CLEAN:
|
||||||
|
tama.hygiene = (uint8_t)min((int)tama.hygiene + 40, 100);
|
||||||
|
setBuddyMood(MOOD_SURPRISED, 3000);
|
||||||
|
dur = 3000;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
overlayAction = a;
|
overlayAction = a;
|
||||||
overlayUntil = millis() + 8000; // show for 8 s
|
overlayUntil = millis() + dur;
|
||||||
Serial.printf("[Action] %s -> %s\n", GNAME[idx], ACTION_LABELS[a]);
|
Serial.printf("[Action] %s -> %s | H:%d P:%d C:%d\n",
|
||||||
|
GNAME[idx], ACTION_LABELS[a],
|
||||||
|
tama.hunger, tama.happiness, tama.hygiene);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleGesture(Gesture g)
|
void handleGesture(Gesture g)
|
||||||
@@ -779,6 +1176,7 @@ void handleGesture(Gesture g)
|
|||||||
if (g == GES_NONE)
|
if (g == GES_NONE)
|
||||||
return;
|
return;
|
||||||
buddy.lastEvent = millis();
|
buddy.lastEvent = millis();
|
||||||
|
if (g_dimmed) setDim(false);
|
||||||
|
|
||||||
int idx = gestureIndex(g);
|
int idx = gestureIndex(g);
|
||||||
if (idx < 0 || idx >= NUM_GESTURES)
|
if (idx < 0 || idx >= NUM_GESTURES)
|
||||||
@@ -789,14 +1187,15 @@ void handleGesture(Gesture g)
|
|||||||
// Execute action (e.g. show datetime)
|
// Execute action (e.g. show datetime)
|
||||||
executeAction(idx);
|
executeAction(idx);
|
||||||
|
|
||||||
// Fire webhook if configured
|
// Set mood: use configured mood if set, otherwise default for this gesture
|
||||||
bool webhookFired = (gConfig[idx].enabled && strlen(gConfig[idx].url) > 0);
|
if (gConfig[idx].mood > 0)
|
||||||
if (webhookFired)
|
setBuddyMood((Mood)gConfig[idx].mood, 4000);
|
||||||
fireWebhook(idx);
|
else
|
||||||
|
|
||||||
// Set mood (webhook config overrides default)
|
|
||||||
if (!webhookFired || gConfig[idx].mood == 0)
|
|
||||||
setBuddyMood(DEFAULT_MOOD[idx], DEFAULT_MOOD_DUR[idx]);
|
setBuddyMood(DEFAULT_MOOD[idx], DEFAULT_MOOD_DUR[idx]);
|
||||||
|
|
||||||
|
// Fire webhook if URL configured
|
||||||
|
if (gConfig[idx].enabled && strlen(gConfig[idx].url) > 0)
|
||||||
|
fireWebhook(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Setup ─────────────────────────────────────────────────────────────────────
|
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -809,8 +1208,9 @@ void setup()
|
|||||||
splash("Desk Buddy", "Budze sie...", "");
|
splash("Desk Buddy", "Budze sie...", "");
|
||||||
delay(600);
|
delay(600);
|
||||||
|
|
||||||
loadConfig();
|
loadAllConfig();
|
||||||
initBuddy();
|
initBuddy();
|
||||||
|
initTama();
|
||||||
|
|
||||||
// Single I2C bus for both devices: SDA=GPIO22, SCL=GPIO23
|
// Single I2C bus for both devices: SDA=GPIO22, SCL=GPIO23
|
||||||
Wire.begin(22, 23);
|
Wire.begin(22, 23);
|
||||||
@@ -845,6 +1245,16 @@ void loop()
|
|||||||
handleGesture(g);
|
handleGesture(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Night dim: 00–05, after 5 s idle → display off
|
||||||
|
static uint32_t lastDimCheck = 0;
|
||||||
|
if (now - lastDimCheck >= 1000) {
|
||||||
|
lastDimCheck = now;
|
||||||
|
struct tm t;
|
||||||
|
bool night = getLocalTime(&t) && t.tm_hour < 5;
|
||||||
|
bool idle = (now - buddy.lastEvent) > 300000UL;
|
||||||
|
setDim(night && idle);
|
||||||
|
}
|
||||||
|
|
||||||
// WiFi keepalive
|
// WiFi keepalive
|
||||||
static uint32_t lastWifi = 0;
|
static uint32_t lastWifi = 0;
|
||||||
if (now - lastWifi > 30000)
|
if (now - lastWifi > 30000)
|
||||||
@@ -854,6 +1264,14 @@ void loop()
|
|||||||
WiFi.reconnect();
|
WiFi.reconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tamagotchi needs (every 10 s — cheap tick check)
|
||||||
|
static uint32_t lastTama = 0;
|
||||||
|
if (now - lastTama >= 10000)
|
||||||
|
{
|
||||||
|
lastTama = now;
|
||||||
|
updateTama();
|
||||||
|
}
|
||||||
|
|
||||||
// Buddy animation state (every 50 ms — no drawing here)
|
// Buddy animation state (every 50 ms — no drawing here)
|
||||||
static uint32_t lastAnim = 0;
|
static uint32_t lastAnim = 0;
|
||||||
if (now - lastAnim >= 50)
|
if (now - lastAnim >= 50)
|
||||||
|
|||||||
Reference in New Issue
Block a user