feat: moods, emotions, needs
This commit is contained in:
+591
-173
@@ -4,17 +4,19 @@
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <Preferences.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RevEng_PAJ7620.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
// ── Hardware ──────────────────────────────────────────────────────────────────
|
||||
// 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);
|
||||
RevEng_PAJ7620 sensor;
|
||||
|
||||
const char *WIFI_SSID = "SSID";
|
||||
const char *WIFI_PASS = "PASSWORD";
|
||||
// WiFi credentials — loaded from LittleFS /config.json at boot
|
||||
static char WIFI_SSID[64] = "";
|
||||
static char WIFI_PASS[64] = "";
|
||||
|
||||
|
||||
// ── Gesture config ─────────────────────────────────────────────────────────────
|
||||
// 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
|
||||
static const char *MOOD_LABELS[] = {
|
||||
"-- 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
|
||||
enum Action
|
||||
{
|
||||
ACTION_NONE = 0,
|
||||
ACTION_NONE = 0,
|
||||
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 char *ACTION_LABELS[] = {"-- brak --", "Data i godzina", "Status WiFi"};
|
||||
static const uint8_t NUM_ACTIONS = 7;
|
||||
static const char *ACTION_LABELS[] = {
|
||||
"-- brak --", "Data i godzina", "Status WiFi",
|
||||
"Nakarm", "Pobaw sie", "Umyj", "Status tamagotchi"};
|
||||
|
||||
struct GestureConfig
|
||||
{
|
||||
@@ -50,7 +59,6 @@ struct GestureConfig
|
||||
};
|
||||
|
||||
GestureConfig gConfig[NUM_GESTURES];
|
||||
Preferences prefs;
|
||||
AsyncWebServer httpServer(80);
|
||||
|
||||
// ── Buddy ─────────────────────────────────────────────────────────────────────
|
||||
@@ -62,7 +70,12 @@ enum Mood
|
||||
MOOD_SURPRISED,
|
||||
MOOD_ANGRY,
|
||||
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
|
||||
{
|
||||
@@ -74,10 +87,12 @@ enum BlinkState
|
||||
|
||||
static const uint8_t EYE_L_X = 38;
|
||||
static const uint8_t EYE_R_X = 90;
|
||||
static const uint8_t EYE_Y = 30;
|
||||
static const uint8_t EYE_RX = 17;
|
||||
static const uint8_t EYE_RY = 15;
|
||||
static const uint8_t EYE_Y = 27;
|
||||
static const uint8_t EYE_RX = 17;
|
||||
static const uint8_t EYE_RY = 15;
|
||||
static const uint8_t PUPIL_R = 6;
|
||||
static const uint8_t MOUTH_X = 64;
|
||||
static const uint8_t MOUTH_Y = 52;
|
||||
|
||||
struct
|
||||
{
|
||||
@@ -91,14 +106,48 @@ struct
|
||||
int8_t pupilDx, pupilDy;
|
||||
int8_t pupilTargetDx, pupilTargetDy;
|
||||
uint32_t nextLook;
|
||||
uint8_t zzzPhase;
|
||||
uint8_t zzzPhase;
|
||||
uint32_t nextZzz;
|
||||
uint32_t nextMicroTremor; // involuntary fixation tremor
|
||||
} 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 ────────────────────────────────────────────────────────────
|
||||
uint32_t overlayUntil = 0; // show overlay until this timestamp
|
||||
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)
|
||||
{
|
||||
buddy.mood = m;
|
||||
@@ -137,110 +186,247 @@ void initBuddy()
|
||||
buddy.blinkState = BLINK_OPEN;
|
||||
buddy.blinkRy = EYE_RY;
|
||||
buddy.lastEvent = millis();
|
||||
buddy.nextBlink = millis() + 3000;
|
||||
buddy.nextLook = millis() + 2000;
|
||||
buddy.nextZzz = millis() + 3000;
|
||||
buddy.nextBlink = millis() + 3000;
|
||||
buddy.nextLook = millis() + 2000;
|
||||
buddy.nextZzz = millis() + 3000;
|
||||
buddy.nextMicroTremor = millis() + 500;
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
int8_t pdx, int8_t pdy, bool isLeft)
|
||||
{
|
||||
if (effRy == 0)
|
||||
return;
|
||||
// Buddy faces the viewer, so buddy's LEFT eye = screen RIGHT (!isLeft)
|
||||
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)
|
||||
{
|
||||
|
||||
case MOOD_HAPPY:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawBox(cx - EYE_RX - 1, cy - effRy - 1, EYE_RX * 2 + 3, effRy + 2);
|
||||
u8g2.setDrawColor(1);
|
||||
// Wide "U" smile + round cheek blush dots
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 7, 10,
|
||||
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 7, 11,
|
||||
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;
|
||||
|
||||
case MOOD_SLEEPY:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
||||
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);
|
||||
}
|
||||
// Thick horizontal bar — tired, barely open
|
||||
u8g2.drawBox(MOUTH_X - 8, MOUTH_Y - 1, 16, 3);
|
||||
break;
|
||||
|
||||
case MOOD_SURPRISED:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX + 2, effRy, U8G2_DRAW_ALL);
|
||||
// Open "O" — filled ring (outline + hollow)
|
||||
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 5, 6, U8G2_DRAW_ALL);
|
||||
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.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;
|
||||
|
||||
case MOOD_ANGRY:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx + pdx, cy + pdy, PUPIL_R);
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 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);
|
||||
// Tight frown with lower lip bar
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 7, 7,
|
||||
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 7, 8,
|
||||
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||
u8g2.drawBox(MOUTH_X - 7, MOUTH_Y + 6, 14, 2);
|
||||
break;
|
||||
|
||||
case MOOD_SAD:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx + pdx, cy + pdy, PUPIL_R);
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 2);
|
||||
if (isLeft)
|
||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 9, cx + EYE_RX - 2, cy - effRy - 4);
|
||||
else
|
||||
u8g2.drawLine(cx - EYE_RX + 2, cy - effRy - 4, cx + EYE_RX - 2, cy - effRy - 9);
|
||||
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);
|
||||
// Deep frown — teardrops on cheeks
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 9, 9,
|
||||
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y + 9, 10,
|
||||
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
|
||||
// teardrops
|
||||
u8g2.drawDisc(MOUTH_X - 18, MOUTH_Y + 5, 2);
|
||||
u8g2.drawLine(MOUTH_X - 18, MOUTH_Y + 7, MOUTH_X - 18, MOUTH_Y + 11);
|
||||
u8g2.drawDisc(MOUTH_X + 18, MOUTH_Y + 5, 2);
|
||||
u8g2.drawLine(MOUTH_X + 18, MOUTH_Y + 7, MOUTH_X + 18, MOUTH_Y + 11);
|
||||
break;
|
||||
|
||||
case MOOD_EXCITED:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawCircle(cx, cy, EYE_RX - 1, U8G2_DRAW_ALL);
|
||||
u8g2.drawLine(cx - 9, cy - 9, cx + 9, cy + 9);
|
||||
u8g2.drawLine(cx + 9, cy - 9, cx - 9, cy + 9);
|
||||
u8g2.drawLine(cx - 11, cy, cx + 11, cy);
|
||||
u8g2.drawLine(cx, cy - 11, cx, cy + 11);
|
||||
u8g2.drawDisc(cx, cy, 3);
|
||||
// Wide open "D" mouth — flat top, arc bottom
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 6, 11,
|
||||
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||
u8g2.drawCircle(MOUTH_X, MOUTH_Y - 6, 12,
|
||||
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||
u8g2.drawBox(MOUTH_X - 12, MOUTH_Y - 7, 24, 2);
|
||||
break;
|
||||
|
||||
default:
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawFilledEllipse(cx, cy, EYE_RX, effRy, U8G2_DRAW_ALL);
|
||||
if (effRy > 3)
|
||||
{
|
||||
int8_t pr = (effRy >= PUPIL_R) ? (int8_t)PUPIL_R : (int8_t)(effRy - 1);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx + pdx, cy + pdy, (uint8_t)pr);
|
||||
if (pr >= 3)
|
||||
{
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawDisc(cx + pdx - 2, cy + pdy - 2, 2);
|
||||
}
|
||||
}
|
||||
case MOOD_HUNGRY:
|
||||
// Open oval mouth — anticipating food
|
||||
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 5, 4, U8G2_DRAW_ALL);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawFilledEllipse(MOUTH_X, MOUTH_Y, 3, 2, U8G2_DRAW_ALL);
|
||||
u8g2.setDrawColor(1);
|
||||
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();
|
||||
}
|
||||
|
||||
// ── 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()
|
||||
{
|
||||
// Action overlay takes priority
|
||||
@@ -336,14 +605,13 @@ void showBuddyScreen()
|
||||
{
|
||||
switch (overlayAction)
|
||||
{
|
||||
case ACTION_DATETIME:
|
||||
showDateTimeScreen();
|
||||
break;
|
||||
case ACTION_WIFI:
|
||||
showWiFiStatusScreen();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case ACTION_DATETIME: showDateTimeScreen(); break;
|
||||
case ACTION_WIFI: showWiFiStatusScreen(); break;
|
||||
case ACTION_FEED: showFeedScreen(); break;
|
||||
case ACTION_PLAY: showPlayScreen(); break;
|
||||
case ACTION_CLEAN: showCleanScreen(); break;
|
||||
case ACTION_STATUS: showTamaStatusScreen(); break;
|
||||
default: break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -353,30 +621,70 @@ void showBuddyScreen()
|
||||
u8g2.setDrawColor(1);
|
||||
|
||||
uint8_t effRy = buddy.blinkRy;
|
||||
if (buddy.mood == MOOD_HAPPY)
|
||||
effRy = min(effRy, (uint8_t)13);
|
||||
if (buddy.mood == MOOD_SLEEPY)
|
||||
effRy = min(effRy, (uint8_t)11);
|
||||
// SURPRISED: eyes open wider than normal max
|
||||
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);
|
||||
|
||||
const char *labels[] = {"", "^_^", "zZz", "o_O", ">_<", "T_T", "*_*"};
|
||||
u8g2.setFont(u8g2_font_5x7_tr);
|
||||
uint8_t lw = u8g2.getStrWidth(labels[buddy.mood]);
|
||||
u8g2.drawStr((128 - lw) / 2, 63, labels[buddy.mood]);
|
||||
drawMouth();
|
||||
|
||||
if (buddy.mood == MOOD_SLEEPY && buddy.zzzPhase > 0)
|
||||
{
|
||||
const char *zStr[] = {"z", "zz", "zzz"};
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
@@ -389,7 +697,8 @@ void updateBuddyAnim()
|
||||
if (buddy.mood == MOOD_NORMAL && now - buddy.lastEvent > 300000UL)
|
||||
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)
|
||||
{
|
||||
@@ -427,20 +736,44 @@ void updateBuddyAnim()
|
||||
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);
|
||||
buddy.pupilTargetDy = (int8_t)random(-4, 5);
|
||||
buddy.nextLook = now + random(1500, 4000);
|
||||
int8_t ddx = buddy.pupilTargetDx - buddy.pupilDx;
|
||||
int8_t ddy = buddy.pupilTargetDy - buddy.pupilDy;
|
||||
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)
|
||||
{
|
||||
@@ -449,51 +782,77 @@ void updateBuddyAnim()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config persistence (NVS via Preferences) ──────────────────────────────────
|
||||
void loadConfig()
|
||||
// ── Config persistence ─────────────────────────────────────────────────────────
|
||||
// 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
|
||||
for (uint8_t i = 0; i < NUM_GESTURES; i++)
|
||||
{
|
||||
char key[16];
|
||||
|
||||
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();
|
||||
File f = LittleFS.open(path, "r");
|
||||
if (!f) { Serial.printf("[Config] %s not found\n", path); return; }
|
||||
if (deserializeJson(doc, f) != DeserializationError::Ok)
|
||||
Serial.printf("[Config] %s parse error\n", path);
|
||||
f.close();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
prefs.begin("buddy", false); // read-write
|
||||
for (uint8_t i = 0; i < NUM_GESTURES; i++)
|
||||
{
|
||||
char key[16];
|
||||
|
||||
snprintf(key, sizeof(key), "wh.%s.url", GKEY[i]);
|
||||
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);
|
||||
JsonDocument doc;
|
||||
for (uint8_t i = 0; i < NUM_GESTURES; i++) {
|
||||
doc[GNAME[i]]["url"] = gConfig[i].url;
|
||||
doc[GNAME[i]]["mood"] = gConfig[i].mood;
|
||||
doc[GNAME[i]]["action"] = gConfig[i].action;
|
||||
doc[GNAME[i]]["enabled"] = gConfig[i].enabled;
|
||||
}
|
||||
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 ───────────────────────────────────────────
|
||||
@@ -529,10 +888,6 @@ void fireWebhook(uint8_t gestureIdx)
|
||||
if (!cfg.enabled || strlen(cfg.url) == 0)
|
||||
return;
|
||||
|
||||
// Set mood before firing (immediate feedback)
|
||||
if (cfg.mood > 0)
|
||||
setBuddyMood((Mood)cfg.mood, 4000);
|
||||
|
||||
// Fire async
|
||||
WebhookTask *t = new WebhookTask;
|
||||
strncpy(t->url, cfg.url, sizeof(t->url) - 1);
|
||||
@@ -590,7 +945,7 @@ static String buildHtml()
|
||||
html += "'></td><td><select name='mood_";
|
||||
html += GKEY[i];
|
||||
html += "'>";
|
||||
for (uint8_t m = 0; m < 7; m++)
|
||||
for (uint8_t m = 0; m < 12; m++)
|
||||
{
|
||||
html += "<option value='";
|
||||
html += m;
|
||||
@@ -700,6 +1055,24 @@ void setupHttpServer()
|
||||
saveConfig();
|
||||
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();
|
||||
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;
|
||||
if (a == ACTION_NONE)
|
||||
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;
|
||||
overlayUntil = millis() + 8000; // show for 8 s
|
||||
Serial.printf("[Action] %s -> %s\n", GNAME[idx], ACTION_LABELS[a]);
|
||||
overlayUntil = millis() + dur;
|
||||
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)
|
||||
@@ -779,6 +1176,7 @@ void handleGesture(Gesture g)
|
||||
if (g == GES_NONE)
|
||||
return;
|
||||
buddy.lastEvent = millis();
|
||||
if (g_dimmed) setDim(false);
|
||||
|
||||
int idx = gestureIndex(g);
|
||||
if (idx < 0 || idx >= NUM_GESTURES)
|
||||
@@ -789,14 +1187,15 @@ void handleGesture(Gesture g)
|
||||
// Execute action (e.g. show datetime)
|
||||
executeAction(idx);
|
||||
|
||||
// Fire webhook if configured
|
||||
bool webhookFired = (gConfig[idx].enabled && strlen(gConfig[idx].url) > 0);
|
||||
if (webhookFired)
|
||||
fireWebhook(idx);
|
||||
|
||||
// Set mood (webhook config overrides default)
|
||||
if (!webhookFired || gConfig[idx].mood == 0)
|
||||
// Set mood: use configured mood if set, otherwise default for this gesture
|
||||
if (gConfig[idx].mood > 0)
|
||||
setBuddyMood((Mood)gConfig[idx].mood, 4000);
|
||||
else
|
||||
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 ─────────────────────────────────────────────────────────────────────
|
||||
@@ -809,8 +1208,9 @@ void setup()
|
||||
splash("Desk Buddy", "Budze sie...", "");
|
||||
delay(600);
|
||||
|
||||
loadConfig();
|
||||
loadAllConfig();
|
||||
initBuddy();
|
||||
initTama();
|
||||
|
||||
// Single I2C bus for both devices: SDA=GPIO22, SCL=GPIO23
|
||||
Wire.begin(22, 23);
|
||||
@@ -845,6 +1245,16 @@ void loop()
|
||||
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
|
||||
static uint32_t lastWifi = 0;
|
||||
if (now - lastWifi > 30000)
|
||||
@@ -854,6 +1264,14 @@ void loop()
|
||||
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)
|
||||
static uint32_t lastAnim = 0;
|
||||
if (now - lastAnim >= 50)
|
||||
|
||||
Reference in New Issue
Block a user