feat: testowanie nastrojów za pomocą rest api
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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 i symuluje potrzeby w stylu Tamagotchi. Konfigurowany przez przeglądarkę.
|
||||
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ę.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,6 +73,21 @@ Buddy ma trzy potrzeby narastające z czasem:
|
||||
|
||||
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](https://open-meteo.com/) — **darmowe, bez klucza API**. Dwuetapowe pobieranie: geocoding (nazwa miasta → współrzędne) + aktualna prognoza.
|
||||
|
||||
### Gesty (PAJ7620)
|
||||
|
||||
Czujnik rozpoznaje 9 gestów ręką:
|
||||
@@ -132,27 +147,56 @@ Po uruchomieniu wejdź przeglądarką na adres IP wypisany w Serial (lub na OLED
|
||||
http://<IP>/
|
||||
```
|
||||
|
||||
Dla każdego gestu można ustawić:
|
||||
- **URL webhoka** — endpoint HTTP POST (pusty = wyłączony)
|
||||
- **Nastrój** — jaki nastrój ustawić po geście (0 = bez zmiany)
|
||||
- **Akcja** — co zrobić/wyświetlić na OLED
|
||||
- **ON** — czy gest jest aktywny
|
||||
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
|
||||
|
||||
```bash
|
||||
# Odczyt konfiguracji gestów
|
||||
GET http://<IP>/api/config
|
||||
# Odczyt / zapis konfiguracji gestów
|
||||
GET http://<IP>/api/config
|
||||
POST http://<IP>/api/config (Content-Type: application/json)
|
||||
|
||||
# Zapis konfiguracji gestów
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"wave": {
|
||||
"url": "http://homeassistant.local:8123/api/webhook/my_hook",
|
||||
"mood": 6,
|
||||
"action": 4,
|
||||
"action": 0,
|
||||
"enabled": true
|
||||
},
|
||||
"up": {
|
||||
@@ -215,6 +259,10 @@ Plik `data/config.json` zawiera wyłącznie konfigurację gestów (bez danych Wi
|
||||
}
|
||||
```
|
||||
|
||||
### Konfiguracja pogody
|
||||
|
||||
Konfiguracja przechowywana w `/weather.json` na LittleFS — zapisywana przez panel webowy, persystuje przez restarty. Nie wymaga reflashowania.
|
||||
|
||||
---
|
||||
|
||||
## Instalacja i build
|
||||
@@ -243,7 +291,7 @@ task flash-monitor # uploadfs + upload + monitor
|
||||
|---------|------|
|
||||
| `task build` | Tylko kompilacja |
|
||||
| `task flash` | Wgraj filesystem + firmware |
|
||||
| `task upload` | Wgraj tylko 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 |
|
||||
@@ -251,8 +299,11 @@ task flash-monitor # uploadfs + upload + monitor
|
||||
| `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 uploadfs` nadpisuje LittleFS — konfiguracja pogody zapisana przez panel webowy zostanie utracona. Użyj `task upload` do aktualizacji samego firmware'u.
|
||||
|
||||
### Zmiana WiFi bez rekompilacji
|
||||
|
||||
```bash
|
||||
@@ -272,13 +323,31 @@ mathieucarbou/ESPAsyncWebServer@^3.3.12
|
||||
|
||||
## Architektura kodu
|
||||
|
||||
Cały projekt mieści się w jednym pliku `src/main.cpp`.
|
||||
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
|
||||
@@ -286,15 +355,16 @@ setup()
|
||||
connectWiFi() → NTP sync
|
||||
setupHttpServer() ← AsyncWebServer port 80
|
||||
|
||||
loop() ← polling
|
||||
loop()
|
||||
sensor.readGesture() ← co 500 ms
|
||||
handleGesture()
|
||||
executeAction() ← tama efekt + overlay OLED
|
||||
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
|
||||
@@ -306,13 +376,23 @@ loop() ← polling
|
||||
| `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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
1. Dodaj wartość do `enum Action` w `main.cpp`
|
||||
2. Zaktualizuj `NUM_ACTIONS` i `ACTION_LABELS[]`
|
||||
1. Dodaj wartość do `enum Action` w `GestureConfig.h`
|
||||
2. Zaktualizuj `NUM_ACTIONS` i `ACTION_LABELS[]` w `GestureConfig.cpp`
|
||||
3. Napisz funkcję `showXxxScreen()`
|
||||
4. Dodaj `case ACTION_XXX:` w `showBuddyScreen()` (sekcja overlay)
|
||||
5. Opcjonalnie: dodaj efekt tama w `executeAction()` (np. zmiana potrzeby)
|
||||
|
||||
@@ -32,7 +32,7 @@ void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs)
|
||||
case MOOD_SAD:
|
||||
b.pupilTargetDx = 0; b.pupilTargetDy = 4; break;
|
||||
case MOOD_ANGRY:
|
||||
b.pupilTargetDx = 2; b.pupilTargetDy = 2; break;
|
||||
b.pupilTargetDx = -2; b.pupilTargetDy = 2; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng)
|
||||
}
|
||||
} else if (b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R) {
|
||||
if (fixated && now >= b.nextLook) {
|
||||
b.pupilTargetDx = (int8_t)rng(-7, 8);
|
||||
b.pupilTargetDx = (int8_t)rng(-3, 11);
|
||||
b.pupilTargetDy = (int8_t)rng(-5, 6);
|
||||
b.nextLook = now + (uint32_t)rng(1500, 4500);
|
||||
}
|
||||
|
||||
+112
-22
@@ -99,7 +99,7 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
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 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);
|
||||
@@ -117,20 +117,28 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
u8g2.drawFilledEllipse(cx, cy, r, ry, U8G2_DRAW_ALL);
|
||||
if (ry >= 6) {
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx + 5, cy - 5, 3);
|
||||
u8g2.drawDisc(cx - 5, cy - 5, 3);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MOOD_ANGRY: {
|
||||
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);
|
||||
// Oko — dolna połowa elipsy (jak sad)
|
||||
uint8_t ry = min(effRy, EYE_RX);
|
||||
u8g2.drawFilledEllipse(cx, cy - 2, EYE_RX, ry,
|
||||
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||
// Źrenica
|
||||
if (ry >= 4) {
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx - 3, cy + 2, 2);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
// Brew ściągnięta do środka (V kształt nad okiem)
|
||||
uint8_t browY = cy - ry - 3;
|
||||
if (isLeft) {
|
||||
u8g2.drawLine(cx - EYE_RX + 2, browY, cx + EYE_RX - 2, browY + 3);
|
||||
} else {
|
||||
u8g2.drawLine(cx - EYE_RX + 2, browY + 3, cx + EYE_RX - 2, browY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -138,6 +146,19 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
uint8_t ry = min(effRy, EYE_RX);
|
||||
u8g2.drawFilledEllipse(cx, cy - 2, EYE_RX, ry,
|
||||
U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
|
||||
// Źrenica
|
||||
if (ry >= 4) {
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.drawDisc(cx - 3, cy + 2, 2);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
// Brew opadająca na zewnątrz (smutna — odwrotnie niż angry)
|
||||
uint8_t browY = cy - ry - 3;
|
||||
if (isLeft) {
|
||||
u8g2.drawLine(cx - EYE_RX + 2, browY + 3, cx + EYE_RX - 2, browY);
|
||||
} else {
|
||||
u8g2.drawLine(cx - EYE_RX + 2, browY, cx + EYE_RX - 2, browY + 3);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MOOD_EXCITED: {
|
||||
@@ -145,10 +166,10 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
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.drawLine(cx - 7, cy - 7, cx + 7, cy + 7);
|
||||
u8g2.drawLine(cx + 7, cy - 7, cx - 7, cy + 7);
|
||||
u8g2.setDrawColor(1);
|
||||
u8g2.drawDisc(cx, cy, 2);
|
||||
u8g2.drawDisc(cx, cy, 5);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -167,8 +188,8 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
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.drawDisc(cx - 4, cy - 4, 3);
|
||||
u8g2.drawDisc(cx + 3, cy + 3, 1);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
break;
|
||||
@@ -178,8 +199,19 @@ static void drawEye(uint8_t cx, uint8_t cy, uint8_t effRy,
|
||||
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.drawDisc(cx + 4, cy - 2, 2);
|
||||
u8g2.drawDisc(cx - 4, cy + 2, 2);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MOOD_WINK_L:
|
||||
case MOOD_WINK_R: {
|
||||
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 - 3, cy - 3, 2);
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
break;
|
||||
@@ -968,10 +1000,41 @@ static String buildHtml()
|
||||
"});"
|
||||
"</script>"
|
||||
|
||||
"<hr style='border-color:#222;margin:16px 0'>"
|
||||
|
||||
// ── Mood test panel ───────────────────────────────────────────────
|
||||
"<h3 style='color:#4f4;margin:0 0 10px'>Test nastroju</h3>"
|
||||
"<div style='margin-bottom:8px;display:flex;flex-wrap:wrap;gap:6px'>"
|
||||
"<button type='button' onclick='tm(0)' style='background:#333'>Normal</button>"
|
||||
"<button type='button' onclick='tm(1)' style='background:#185'>Happy</button>"
|
||||
"<button type='button' onclick='tm(2)' style='background:#226'>Sleepy</button>"
|
||||
"<button type='button' onclick='tm(3)' style='background:#550'>Surprised</button>"
|
||||
"<button type='button' onclick='tm(4)' style='background:#622'>Angry</button>"
|
||||
"<button type='button' onclick='tm(5)' style='background:#246'>Sad</button>"
|
||||
"<button type='button' onclick='tm(6)' style='background:#185'>Excited</button>"
|
||||
"<button type='button' onclick='tm(7)' style='background:#444'>Wink L</button>"
|
||||
"<button type='button' onclick='tm(8)' style='background:#444'>Wink R</button>"
|
||||
"<button type='button' onclick='tm(9)' style='background:#532'>Hungry</button>"
|
||||
"<button type='button' onclick='tm(10)' style='background:#245'>Playful</button>"
|
||||
"<button type='button' onclick='tm(11)' style='background:#432'>Dirty</button>"
|
||||
"</div>"
|
||||
"<div id='mmsg' style='color:#4f4;font-size:12px;height:16px'></div>"
|
||||
"<script>"
|
||||
"function tm(m){"
|
||||
"fetch('/api/mood/test?m='+m,{method:'POST'})"
|
||||
".then(r=>r.json()).then(d=>{"
|
||||
"var el=document.getElementById('mmsg');"
|
||||
"el.textContent=d.msg||'OK';"
|
||||
"setTimeout(function(){el.textContent='';},3000);"
|
||||
"});"
|
||||
"}"
|
||||
"</script>"
|
||||
|
||||
"<hr style='border-color:#222;margin:16px 0'>"
|
||||
"<p style='color:#555;font-size:11px'>GET /api/config POST /api/config (JSON)"
|
||||
" | GET /api/tama POST /api/tama/{feed,play,clean}"
|
||||
" | GET /api/weather POST /weather/save</p>"
|
||||
" | GET /api/weather POST /weather/save"
|
||||
" | POST /api/mood/test?m={0-11}</p>"
|
||||
"</body></html>";
|
||||
return html;
|
||||
}
|
||||
@@ -1104,6 +1167,19 @@ void setupHttpServer()
|
||||
req->send(200, "application/json", buf);
|
||||
});
|
||||
|
||||
// POST /api/mood/test?m=N — ustaw nastoj testowy na 10s
|
||||
httpServer.on("/api/mood/test", HTTP_POST, [](AsyncWebServerRequest *req) {
|
||||
uint32_t now = millis();
|
||||
uint8_t m = 0;
|
||||
if (req->hasParam("m"))
|
||||
m = (uint8_t)constrain(req->getParam("m")->value().toInt(), 0, 11);
|
||||
setBuddyMood(buddy, (Mood)m, now, 10000);
|
||||
Serial.printf("[Web] Mood test: %s (%d)\n", MOOD_LABELS[m], m);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "{\"ok\":true,\"msg\":\"Nastoj: %s (10s)\"}", MOOD_LABELS[m]);
|
||||
req->send(200, "application/json", buf);
|
||||
});
|
||||
|
||||
// GET /api/weather — current weather data + config
|
||||
httpServer.on("/api/weather", HTTP_GET, [](AsyncWebServerRequest *req) {
|
||||
char buf[180];
|
||||
@@ -1271,10 +1347,24 @@ void loop()
|
||||
setDim(night && idle);
|
||||
}
|
||||
|
||||
static uint32_t lastWifi = 0;
|
||||
if (now - lastWifi > 30000) {
|
||||
lastWifi = now;
|
||||
if (WiFi.status() != WL_CONNECTED) WiFi.reconnect();
|
||||
// WiFi watchdog: próba reconnect co 30 s, restart po 5 min bez połączenia
|
||||
static uint32_t lastWifiCheck = 0;
|
||||
static uint32_t wifiDownSince = 0; // 0 = połączony lub nie śledzony
|
||||
if (now - lastWifiCheck > 30000) {
|
||||
lastWifiCheck = now;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
wifiDownSince = 0;
|
||||
} else {
|
||||
if (wifiDownSince == 0) wifiDownSince = now ? now : 1;
|
||||
uint32_t downMs = now - wifiDownSince;
|
||||
Serial.printf("[WiFi] Brak polaczenia od %lu s, proba reconnect...\n", downMs / 1000);
|
||||
WiFi.reconnect();
|
||||
if (downMs >= 300000UL) {
|
||||
Serial.println("[WiFi] Brak WiFi > 5 min — restart");
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weather: show face periodically, re-fetch at most every 60 s
|
||||
|
||||
Reference in New Issue
Block a user